ZENLLC commited on
Commit
88de539
·
verified ·
1 Parent(s): 76ab2e6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +113 -357
app.py CHANGED
@@ -2,7 +2,6 @@ import json
2
  import math
3
  import os
4
  import time
5
- import base64
6
  import random
7
  import tempfile
8
  import urllib.request
@@ -20,22 +19,11 @@ import gradio as gr
20
  # ============================================================
21
  # ZEN Orchestrator Sandbox — Business-grade Agent Simulation
22
  # ============================================================
23
- # What this is:
24
- # - An office-world simulator that looks/feels like a "game" (SVG arena)
25
- # - Agents move through an environment while executing task backlogs
26
- # - You can speed time up/down (ticks represent hours/days/weeks)
27
- # - Everything logs into "Run Data" at the bottom + CSV download
28
- # - Optional: connect agents to an OpenAI-compatible endpoint via API key
29
- #
30
- # Constraints:
31
- # - Only uses: gradio, numpy, pillow, pandas (+ stdlib)
32
- # - No hidden modules, no missing files, no broken imports
33
  # ============================================================
34
 
35
-
36
- # -----------------------------
37
- # Visual Config
38
- # -----------------------------
39
  GRID_W, GRID_H = 32, 20
40
  TILE = 22
41
  HUD_H = 70
@@ -48,7 +36,6 @@ COL_GRID = "rgba(255,255,255,0.06)"
48
  COL_TEXT = "rgba(235,240,255,0.92)"
49
  COL_TEXT_DIM = "rgba(235,240,255,0.72)"
50
 
51
- # Tile types
52
  EMPTY = 0
53
  WALL = 1
54
  DESK = 2
@@ -57,16 +44,6 @@ SERVER = 4
57
  INCIDENT = 5
58
  TASK_NODE = 6
59
 
60
- TILE_NAME = {
61
- EMPTY: "Empty",
62
- WALL: "Wall",
63
- DESK: "Desk",
64
- MEETING: "Meeting Room",
65
- SERVER: "Server Rack",
66
- INCIDENT: "Incident",
67
- TASK_NODE: "Task Node",
68
- }
69
-
70
  TILE_COL = {
71
  EMPTY: "#162044",
72
  WALL: "#cdd2e6",
@@ -82,25 +59,16 @@ AGENT_COLORS = [
82
  "#ff9b6b", "#c7d2fe", "#a0ffd9", "#ffb0b0",
83
  ]
84
 
85
-
86
- # -----------------------------
87
- # Model Pricing (editable defaults)
88
- # -----------------------------
89
  DEFAULT_MODEL_PRICING = {
90
- # dollars per 1M tokens
91
  "Simulated-Local": {"in": 0.00, "out": 0.00},
92
  "gpt-4o-mini": {"in": 0.15, "out": 0.60},
93
  "gpt-4o": {"in": 5.00, "out": 15.00},
94
- "gpt-5": {"in": 5.00, "out": 15.00}, # placeholder
95
  }
96
 
97
- # OpenAI compatible default base URL
98
  DEFAULT_OAI_BASE = "https://api.openai.com/v1"
99
 
100
 
101
- # -----------------------------
102
- # Helpers
103
- # -----------------------------
104
  def clamp(v, lo, hi):
105
  return lo if v < lo else hi if v > hi else v
106
 
@@ -113,7 +81,6 @@ def now_iso():
113
  return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
114
 
115
  def est_tokens(text: str) -> int:
116
- # crude but stable estimate: ~4 chars per token
117
  if not text:
118
  return 0
119
  return max(1, int(len(text) / 4))
@@ -130,77 +97,61 @@ def to_csv_download(df: pd.DataFrame) -> str:
130
  return tmp.name
131
 
132
 
133
- # -----------------------------
134
- # Data Models
135
- # -----------------------------
136
  @dataclass
137
  class Task:
138
  id: str
139
  title: str
140
  description: str
141
- priority: int = 3 # 1-5
142
- difficulty: int = 3 # 1-5
143
  est_hours: float = 8.0
144
  created_step: int = 0
145
- status: str = "backlog" # backlog|in_progress|blocked|done
146
  assigned_to: Optional[str] = None
147
- progress: float = 0.0 # 0..1
148
  blockers: List[str] = field(default_factory=list)
149
 
150
  @dataclass
151
  class Agent:
152
  name: str
153
  model: str
154
- key_group: str # "none" or "key1" etc
155
  x: int
156
  y: int
157
  energy: float = 100.0
158
  role: str = "Generalist"
159
- state: str = "idle" # idle|working|moving|blocked
160
  current_task_id: Optional[str] = None
161
  thoughts: str = ""
162
  last_action: str = ""
163
  tokens_in: int = 0
164
  tokens_out: int = 0
165
  cost_usd: float = 0.0
166
- compute_s: float = 0.0 # wallclock spent "thinking"
167
 
168
  @dataclass
169
  class World:
170
  seed: int = 1337
171
  step: int = 0
172
  sim_time_hours: float = 0.0
173
-
174
- # time controls
175
- tick_hours: float = 4.0 # each tick advances this many simulated hours
176
- difficulty: int = 3 # 1-5 global difficulty
177
- incident_rate: float = 0.07 # per tick
178
-
179
- # environment
180
  grid: List[List[int]] = field(default_factory=list)
181
  agents: Dict[str, Agent] = field(default_factory=dict)
182
  tasks: Dict[str, Task] = field(default_factory=dict)
183
-
184
- # logging
185
  events: List[str] = field(default_factory=list)
186
  runlog: List[Dict[str, Any]] = field(default_factory=list)
187
-
188
- # KPI counters
189
  incidents_open: int = 0
190
  incidents_resolved: int = 0
191
  tasks_done: int = 0
192
-
193
  done: bool = False
194
 
195
 
196
- # -----------------------------
197
- # Environment Builder
198
- # -----------------------------
199
  def build_office(seed: int) -> List[List[int]]:
200
  r = make_rng(seed)
201
  g = [[EMPTY for _ in range(GRID_W)] for _ in range(GRID_H)]
202
 
203
- # border walls
204
  for x in range(GRID_W):
205
  g[0][x] = WALL
206
  g[GRID_H - 1][x] = WALL
@@ -208,36 +159,27 @@ def build_office(seed: int) -> List[List[int]]:
208
  g[y][0] = WALL
209
  g[y][GRID_W - 1] = WALL
210
 
211
- # rooms blocks
212
  def rect(x0, y0, x1, y1, tile):
213
  for y in range(y0, y1 + 1):
214
  for x in range(x0, x1 + 1):
215
  if 1 <= x < GRID_W - 1 and 1 <= y < GRID_H - 1:
216
  g[y][x] = tile
217
 
218
- # main open office
219
  rect(2, 2, GRID_W - 3, GRID_H - 3, EMPTY)
220
-
221
- # meeting rooms
222
  rect(3, 3, 10, 7, MEETING)
223
  rect(GRID_W - 11, 3, GRID_W - 4, 7, MEETING)
224
-
225
- # server room
226
  rect(GRID_W - 10, GRID_H - 8, GRID_W - 4, GRID_H - 3, SERVER)
227
 
228
- # desks grid
229
  for y in range(9, GRID_H - 10):
230
  for x in range(4, GRID_W - 12):
231
  if (x % 3 == 1) and (y % 2 == 0):
232
  g[y][x] = DESK
233
 
234
- # task nodes (places where work happens)
235
  nodes = [(6, GRID_H - 5), (GRID_W // 2, GRID_H // 2), (GRID_W - 14, 10)]
236
  for (x, y) in nodes:
237
  if 1 <= x < GRID_W - 1 and 1 <= y < GRID_H - 1:
238
  g[y][x] = TASK_NODE
239
 
240
- # random inner walls for navigation texture
241
  for _ in range(22):
242
  x = r.randint(3, GRID_W - 4)
243
  y = r.randint(8, GRID_H - 9)
@@ -254,10 +196,6 @@ def random_walkable_cell(g: List[List[int]], r: random.Random) -> Tuple[int, int
254
  opts.append((x, y))
255
  return r.choice(opts) if opts else (2, 2)
256
 
257
-
258
- # -----------------------------
259
- # Initialization
260
- # -----------------------------
261
  def init_world(seed: int) -> World:
262
  seed = int(seed)
263
  g = build_office(seed)
@@ -268,13 +206,7 @@ def init_world(seed: int) -> World:
268
  def add_agent(w: World, name: str, model: str, key_group: str, role: str, seed_bump: int = 0):
269
  r = make_rng(w.seed + w.step * 17 + seed_bump)
270
  x, y = random_walkable_cell(w.grid, r)
271
- w.agents[name] = Agent(
272
- name=name,
273
- model=model,
274
- key_group=key_group,
275
- x=x, y=y,
276
- role=role
277
- )
278
  w.events.append(f"[t={w.step}] Agent added: {name} | model={model} | key_group={key_group} | role={role}")
279
 
280
  def add_task(w: World, title: str, description: str, priority: int, difficulty: int, est_hours: float):
@@ -292,9 +224,6 @@ def add_task(w: World, title: str, description: str, priority: int, difficulty:
292
  return tid
293
 
294
 
295
- # -----------------------------
296
- # Pathing (simple BFS)
297
- # -----------------------------
298
  DIRS4 = [(1,0), (0,1), (-1,0), (0,-1)]
299
 
300
  def in_bounds(x, y):
@@ -331,10 +260,6 @@ def bfs_next_step(grid: List[List[int]], start: Tuple[int,int], goal: Tuple[int,
331
  return cur
332
 
333
 
334
- # -----------------------------
335
- # OpenAI-Compatible Call (optional)
336
- # - Uses urllib from stdlib; no new deps
337
- # -----------------------------
338
  def oai_chat_completion(base_url: str, api_key: str, model: str, messages: List[Dict[str,str]], timeout_s: int = 25) -> Dict[str, Any]:
339
  url = base_url.rstrip("/") + "/chat/completions"
340
  payload = json.dumps({
@@ -346,10 +271,7 @@ def oai_chat_completion(base_url: str, api_key: str, model: str, messages: List[
346
  req = urllib.request.Request(
347
  url,
348
  data=payload,
349
- headers={
350
- "Content-Type": "application/json",
351
- "Authorization": f"Bearer {api_key}",
352
- },
353
  method="POST",
354
  )
355
  try:
@@ -365,22 +287,12 @@ def oai_chat_completion(base_url: str, api_key: str, model: str, messages: List[
365
  except Exception as e:
366
  return {"error": {"message": str(e)}}
367
 
368
-
369
- # -----------------------------
370
- # Costing
371
- # - "Accurate" if provider returns usage tokens
372
- # - Otherwise estimate tokens from strings
373
- # -----------------------------
374
  def price_for(model_pricing: Dict[str, Dict[str,float]], model: str, tokens_in: int, tokens_out: int) -> float:
375
  p = model_pricing.get(model) or model_pricing.get("Simulated-Local") or {"in":0.0, "out":0.0}
376
  return (tokens_in / 1_000_000.0) * float(p.get("in", 0.0)) + (tokens_out / 1_000_000.0) * float(p.get("out", 0.0))
377
 
378
 
379
- # -----------------------------
380
- # Agent Policy
381
- # -----------------------------
382
  def choose_task_for_agent(w: World, agent: Agent) -> Optional[str]:
383
- # pick highest priority backlog task; tie-breaker: oldest created_step
384
  backlog = [t for t in w.tasks.values() if t.status in ("backlog", "blocked")]
385
  if not backlog:
386
  return None
@@ -388,16 +300,13 @@ def choose_task_for_agent(w: World, agent: Agent) -> Optional[str]:
388
  return backlog[0].id
389
 
390
  def maybe_generate_incident(w: World, r: random.Random):
391
- # more difficulty => more incidents
392
  rate = w.incident_rate * (0.6 + 0.25 * w.difficulty)
393
  if r.random() < rate:
394
- # drop incident tile somewhere
395
  x, y = random_walkable_cell(w.grid, r)
396
  if w.grid[y][x] != WALL:
397
  w.grid[y][x] = INCIDENT
398
  w.incidents_open += 1
399
  w.events.append(f"[t={w.step}] INCIDENT spawned at ({x},{y})")
400
- # incidents also create a task
401
  add_task(
402
  w,
403
  title="Handle incident",
@@ -427,74 +336,32 @@ def nearest_task_node(w: World, ax: int, ay: int) -> Tuple[int,int]:
427
  return nodes[0]
428
 
429
 
430
- # -----------------------------
431
- # "Thinking" / Action
432
- # -----------------------------
433
  def simulated_reasoning(agent: Agent, task: Task, w: World) -> Tuple[str, str, int, int, float]:
434
- """
435
- Returns: (thoughts, action_summary, tokens_in, tokens_out, compute_s)
436
- """
437
- # pretend compute grows with difficulty and task difficulty
438
  base = 0.08 + 0.04 * w.difficulty + 0.03 * task.difficulty
439
  compute_s = clamp(base, 0.05, 0.6)
440
-
441
- # craft stable pseudo-thoughts
442
  thoughts = (
443
- f"Assessing '{task.title}'. Priority={task.priority}, difficulty={task.difficulty}. "
444
- f"Plan: break into steps, execute, verify, document."
445
- )
446
- action = (
447
- f"Worked on {task.id}: progressed implementation, wrote notes, checked blockers."
448
  )
449
-
450
  tin = est_tokens(task.title + " " + task.description) + 30
451
  tout = est_tokens(thoughts + " " + action) + 40
452
  return thoughts, action, tin, tout, compute_s
453
 
454
- def api_reasoning(
455
- agent: Agent,
456
- task: Task,
457
- w: World,
458
- base_url: str,
459
- api_key: str,
460
- model: str,
461
- context_prompt: str
462
- ) -> Tuple[str, str, int, int, float, Optional[str]]:
463
- """
464
- Returns: thoughts, action, tokens_in, tokens_out, compute_s, error
465
- """
466
  t0 = time.time()
467
-
468
  sys = (
469
  "You are an autonomous business operations agent in a multi-agent simulation. "
470
  "Return a JSON object with keys: thoughts, action, blockers (list), progress_delta (0..1). "
471
  "Keep thoughts short and action concrete."
472
  )
473
  user = {
474
- "simulation": {
475
- "step": w.step,
476
- "sim_time_hours": w.sim_time_hours,
477
- "global_difficulty": w.difficulty,
478
- "open_incidents": w.incidents_open,
479
- },
480
- "agent": {
481
- "name": agent.name,
482
- "role": agent.role,
483
- "energy": agent.energy,
484
- },
485
  "task": asdict(task),
486
- "context": context_prompt[:1400] if context_prompt else "",
487
  }
488
-
489
- resp = oai_chat_completion(
490
- base_url=base_url,
491
- api_key=api_key,
492
- model=model,
493
- messages=[
494
- {"role": "system", "content": sys},
495
- {"role": "user", "content": json.dumps(user)},
496
- ]
497
- )
498
  compute_s = float(time.time() - t0)
499
 
500
  if "error" in resp:
@@ -514,7 +381,6 @@ def api_reasoning(
514
 
515
  obj = safe_json(content, fallback=None)
516
  if not isinstance(obj, dict):
517
- # fallback parse: treat raw as action
518
  thoughts = "Provider returned non-JSON; using fallback."
519
  action = content[:400]
520
  tin = usage_in if isinstance(usage_in, int) else est_tokens(sys + json.dumps(user))
@@ -526,7 +392,6 @@ def api_reasoning(
526
  blockers = obj.get("blockers", [])
527
  progress_delta = obj.get("progress_delta", 0.0)
528
 
529
- # Apply structured results
530
  if isinstance(blockers, list) and blockers:
531
  task.blockers = [str(b)[:80] for b in blockers][:5]
532
  task.status = "blocked"
@@ -536,8 +401,7 @@ def api_reasoning(
536
  task.status = "in_progress"
537
 
538
  try:
539
- pdlt = float(progress_delta)
540
- task.progress = clamp(task.progress + pdlt, 0.0, 1.0)
541
  except Exception:
542
  pass
543
 
@@ -546,24 +410,12 @@ def api_reasoning(
546
  return thoughts, action, tin, tout, compute_s, None
547
 
548
 
549
- # -----------------------------
550
- # Core Tick
551
- # -----------------------------
552
- def step_agent(
553
- w: World,
554
- agent: Agent,
555
- r: random.Random,
556
- model_pricing: Dict[str, Dict[str, float]],
557
- keyrings: Dict[str, str],
558
- base_url: str,
559
- context_prompt: str
560
- ):
561
  if agent.energy <= 0:
562
  agent.state = "blocked"
563
  agent.last_action = "Out of energy"
564
  return
565
 
566
- # assign task if none
567
  if agent.current_task_id is None or agent.current_task_id not in w.tasks:
568
  tid = choose_task_for_agent(w, agent)
569
  if tid is None:
@@ -578,11 +430,9 @@ def step_agent(
578
 
579
  task = w.tasks[agent.current_task_id]
580
 
581
- # movement target: incidents -> server room -> meeting -> task node
582
  incs = incident_positions(w)
583
- target = None
584
  if incs and task.priority >= 5:
585
- incs.sort(key=lambda p: abs(p[0]-agent.x) + abs(p[1]-agent.y))
586
  target = incs[0]
587
  else:
588
  target = nearest_task_node(w, agent.x, agent.y)
@@ -595,46 +445,32 @@ def step_agent(
595
  agent.energy = max(0.0, agent.energy - 0.8)
596
  return
597
 
598
- # do work
599
  agent.state = "working"
600
 
601
  if agent.model == "Simulated-Local" or agent.key_group == "none":
602
  thoughts, action, tin, tout, compute_s = simulated_reasoning(agent, task, w)
603
  err = None
604
- else:
605
- key = keyrings.get(agent.key_group, "")
606
- if not key:
607
- thoughts, action, tin, tout, compute_s = simulated_reasoning(agent, task, w)
608
- err = f"No key found for group '{agent.key_group}', used local simulation."
609
- else:
610
- thoughts, action, tin, tout, compute_s, err = api_reasoning(
611
- agent=agent,
612
- task=task,
613
- w=w,
614
- base_url=base_url,
615
- api_key=key,
616
- model=agent.model,
617
- context_prompt=context_prompt
618
- )
619
-
620
- # apply progress if local sim
621
- if agent.model == "Simulated-Local" or agent.key_group == "none":
622
- # harder tasks progress slower; difficulty scales
623
  speed = 0.08 / (0.6 + 0.25 * w.difficulty + 0.30 * task.difficulty)
624
  task.progress = clamp(task.progress + speed, 0.0, 1.0)
625
  if task.progress < 1.0:
626
  task.status = "in_progress"
 
 
 
 
 
 
 
 
 
627
 
628
- # complete
629
  if task.progress >= 1.0 and task.status != "done":
630
  task.status = "done"
631
  w.tasks_done += 1
632
  w.events.append(f"[t={w.step}] DONE {task.id}: {task.title}")
633
  agent.current_task_id = None
634
 
635
- # resolve incident if task was incident-related
636
  if "incident" in task.title.lower():
637
- # remove one incident tile if any
638
  incs = incident_positions(w)
639
  if incs:
640
  x, y = incs[0]
@@ -643,18 +479,14 @@ def step_agent(
643
  w.incidents_resolved += 1
644
  w.events.append(f"[t={w.step}] Incident resolved at ({x},{y})")
645
 
646
- # update tokens/cost/compute
647
  agent.thoughts = thoughts
648
  agent.last_action = action if action else agent.last_action
649
  agent.tokens_in += int(tin)
650
  agent.tokens_out += int(tout)
651
  agent.compute_s += float(compute_s)
652
  agent.cost_usd += price_for(model_pricing, agent.model, int(tin), int(tout))
653
-
654
- # energy drain
655
  agent.energy = max(0.0, agent.energy - (0.8 + 0.15 * w.difficulty + 0.12 * task.difficulty))
656
 
657
- # runlog row
658
  w.runlog.append({
659
  "step": w.step,
660
  "sim_time_hours": round(w.sim_time_hours, 2),
@@ -675,57 +507,33 @@ def step_agent(
675
  })
676
 
677
 
678
- def tick(
679
- w: World,
680
- r: random.Random,
681
- model_pricing: Dict[str, Dict[str, float]],
682
- keyrings: Dict[str, str],
683
- base_url: str,
684
- context_prompt: str,
685
- max_log: int = 4000,
686
- ):
687
  if w.done:
688
  return
689
-
690
- # incidents
691
  maybe_generate_incident(w, r)
692
-
693
- # agent step order: low energy last
694
  agents = list(w.agents.values())
695
  agents.sort(key=lambda a: (a.energy, a.name))
696
-
697
  for ag in agents:
698
  step_agent(w, ag, r, model_pricing, keyrings, base_url, context_prompt)
699
-
700
- # advance time
701
  w.step += 1
702
  w.sim_time_hours += float(w.tick_hours)
703
-
704
- # prune logs
705
  if len(w.events) > 250:
706
  w.events = w.events[-250:]
707
  if len(w.runlog) > max_log:
708
  w.runlog = w.runlog[-max_log:]
709
 
710
 
711
- # -----------------------------
712
- # KPIs
713
- # -----------------------------
714
  def compute_kpis(w: World) -> Dict[str, Any]:
715
  backlog = sum(1 for t in w.tasks.values() if t.status == "backlog")
716
  inprog = sum(1 for t in w.tasks.values() if t.status == "in_progress")
717
  blocked = sum(1 for t in w.tasks.values() if t.status == "blocked")
718
  done = sum(1 for t in w.tasks.values() if t.status == "done")
719
-
720
  total_cost = sum(a.cost_usd for a in w.agents.values())
721
  total_tokens_in = sum(a.tokens_in for a in w.agents.values())
722
  total_tokens_out = sum(a.tokens_out for a in w.agents.values())
723
  total_compute = sum(a.compute_s for a in w.agents.values())
724
-
725
- # throughput: tasks done per simulated day
726
  days = max(1e-6, w.sim_time_hours / 24.0)
727
  tpd = done / days
728
-
729
  return {
730
  "sim_time_days": round(w.sim_time_hours / 24.0, 2),
731
  "agents": len(w.agents),
@@ -744,20 +552,14 @@ def compute_kpis(w: World) -> Dict[str, Any]:
744
  }
745
 
746
 
747
- # -----------------------------
748
- # SVG Renderer
749
- # -----------------------------
750
  def svg_render(w: World) -> str:
751
  k = compute_kpis(w)
752
-
753
  headline = (
754
  f"ZEN Orchestrator Sandbox • step={w.step} • sim_days={k['sim_time_days']} • "
755
  f"agents={k['agents']} • done={k['tasks_done']} • backlog={k['tasks_backlog']} • "
756
  f"incidents_open={k['incidents_open']} • cost=${k['cost_usd_total']}"
757
  )
758
- detail = (
759
- f"tick_hours={w.tick_hours} • difficulty={w.difficulty} • incident_rate={round(w.incident_rate,3)}"
760
- )
761
 
762
  css = f"""
763
  <style>
@@ -791,17 +593,12 @@ def svg_render(w: World) -> str:
791
  50% {{ transform: scale(1.15); opacity: 0.26; }}
792
  100% {{ transform: scale(1.0); opacity: 0.14; }}
793
  }}
794
- .badge {{
795
- fill: rgba(15,23,51,0.72);
796
- stroke: rgba(170,195,255,0.16);
797
- stroke-width: 1;
798
  }}
799
  .tile {{
800
  shape-rendering: crispEdges;
801
  }}
802
- .tag {{
803
- fill: rgba(0,0,0,0.38);
804
- }}
805
  </style>
806
  """
807
 
@@ -815,7 +612,6 @@ def svg_render(w: World) -> str:
815
  <text class="hud hudSmall" x="16" y="52" font-size="12">{detail}</text>
816
  """]
817
 
818
- # tiles
819
  for y in range(GRID_H):
820
  for x in range(GRID_W):
821
  t = w.grid[y][x]
@@ -823,17 +619,10 @@ def svg_render(w: World) -> str:
823
  px = x * TILE
824
  py = HUD_H + y * TILE
825
  parts.append(f'<rect class="tile" x="{px}" y="{py}" width="{TILE}" height="{TILE}" fill="{col}"/>')
826
-
827
- # tile glyphs
828
- if t == SERVER:
829
- parts.append(f'<rect x="{px+6}" y="{py+5}" width="{TILE-12}" height="{TILE-10}" rx="4" fill="rgba(0,0,0,0.28)"/>')
830
- if t == MEETING:
831
- parts.append(f'<circle cx="{px+TILE/2}" cy="{py+TILE/2}" r="5" fill="rgba(0,0,0,0.28)"/>')
832
  if t == INCIDENT:
833
  parts.append(f'<circle cx="{px+TILE/2}" cy="{py+TILE/2}" r="7" fill="rgba(0,0,0,0.25)"/>')
834
  parts.append(f'<circle cx="{px+TILE/2}" cy="{py+TILE/2}" r="4" fill="rgba(255,255,255,0.65)"/>')
835
 
836
- # gridlines subtle
837
  for x in range(GRID_W + 1):
838
  px = x * TILE
839
  parts.append(f'<line class="gridline" x1="{px}" y1="{HUD_H}" x2="{px}" y2="{SVG_H}"/>')
@@ -841,7 +630,6 @@ def svg_render(w: World) -> str:
841
  py = HUD_H + y * TILE
842
  parts.append(f'<line class="gridline" x1="0" y1="{py}" x2="{SVG_W}" y2="{py}"/>')
843
 
844
- # agents
845
  for i, ag in enumerate(w.agents.values()):
846
  col = AGENT_COLORS[i % len(AGENT_COLORS)]
847
  px = ag.x * TILE
@@ -850,28 +638,21 @@ def svg_render(w: World) -> str:
850
  <g class="agent" style="transform: translate({px}px, {py}px);">
851
  <circle class="pulse" cx="{TILE/2}" cy="{TILE/2}" r="{TILE*0.48}" fill="{col}"></circle>
852
  <circle cx="{TILE/2}" cy="{TILE/2}" r="{TILE*0.34}" fill="{col}" opacity="0.98"></circle>
 
 
 
853
  """)
854
-
855
- # state tag
856
- parts.append(f'<rect class="tag" x="{TILE*0.10}" y="{TILE*0.08}" width="{TILE*0.80}" height="14" rx="7"/>')
857
- parts.append(f'<text x="{TILE/2}" y="{TILE*0.08 + 11}" text-anchor="middle" font-size="9" fill="rgba(235,240,255,0.90)" font-family="ui-sans-serif, system-ui">{ag.name}</text>')
858
-
859
- # energy bar
860
  bar_w = TILE * 0.80
861
  bx = TILE/2 - bar_w/2
862
  by = TILE * 0.82
863
  parts.append(f'<rect x="{bx}" y="{by}" width="{bar_w}" height="6" rx="4" fill="rgba(255,255,255,0.12)"/>')
864
  parts.append(f'<rect x="{bx}" y="{by}" width="{bar_w*(clamp(ag.energy,0,100)/100.0)}" height="6" rx="4" fill="rgba(122,255,200,0.85)"/>')
865
-
866
  parts.append("</g>")
867
 
868
  parts.append("</svg></div>")
869
  return "".join(parts)
870
 
871
 
872
- # -----------------------------
873
- # UI Helpers
874
- # -----------------------------
875
  def agents_text(w: World) -> str:
876
  lines = []
877
  for ag in w.agents.values():
@@ -884,7 +665,6 @@ def agents_text(w: World) -> str:
884
  return "\n".join(lines) if lines else "(no agents yet)"
885
 
886
  def tasks_text(w: World) -> str:
887
- # show top tasks by priority and status
888
  tasks = list(w.tasks.values())
889
  tasks.sort(key=lambda t: (t.status != "done", -t.priority, t.created_step))
890
  out = []
@@ -900,8 +680,7 @@ def events_text(w: World) -> str:
900
  return "\n".join(w.events[-20:]) if w.events else ""
901
 
902
  def kpis_text(w: World) -> str:
903
- k = compute_kpis(w)
904
- return json.dumps(k, indent=2)
905
 
906
  def run_data_df(w: World, rows: int) -> pd.DataFrame:
907
  rows = int(max(10, rows))
@@ -910,35 +689,43 @@ def run_data_df(w: World, rows: int) -> pd.DataFrame:
910
  "step","sim_time_hours","agent","role","model","key_group","task_id","task_status","task_progress",
911
  "action","thoughts","tokens_in","tokens_out","cost_usd","compute_s","error"
912
  ])
913
- data = w.runlog[-rows:]
914
- return pd.DataFrame(data)
915
 
916
  def ui_refresh(w: World, run_rows: int):
917
- arena = svg_render(w)
918
- agents_box = agents_text(w)
919
- tasks_box = tasks_text(w)
920
- events_box = events_text(w)
921
- kpi_box = kpis_text(w)
922
- df = run_data_df(w, run_rows)
923
- return arena, agents_box, tasks_box, events_box, kpi_box, df
924
-
925
-
926
- # -----------------------------
927
- # Gradio App
928
- # -----------------------------
929
  TITLE = "ZEN Orchestrator Sandbox — Business-grade Agent Orchestra Simulator"
930
 
931
  with gr.Blocks(title=TITLE) as demo:
932
  gr.Markdown(
933
  f"## {TITLE}\n"
934
- "A **business-oriented multi-agent simulation** with **game-like visuals**, time controls, run logging, and optional model keys.\n"
935
- "You can run fully **without keys** (local simulation), or attach an **OpenAI-compatible endpoint** for live model calls."
936
  )
937
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
  w_state = gr.State(init_world(1337))
939
  autoplay_on = gr.State(False)
940
-
941
- # keyring and pricing live state as JSON text fields
942
  keyrings_state = gr.State({"none": ""})
943
  pricing_state = gr.State(DEFAULT_MODEL_PRICING)
944
 
@@ -956,14 +743,19 @@ with gr.Blocks(title=TITLE) as demo:
956
  with gr.Row():
957
  runlog_rows = gr.Slider(50, 1500, value=250, step=50, label="Run Data rows to display")
958
  download_btn = gr.Button("Download Run Data CSV")
959
- run_data = gr.Dataframe(label="Run Data", interactive=False, wrap=True, height=320)
 
 
 
 
 
960
  download_file = gr.File(label="CSV Download", interactive=False)
961
 
962
  gr.Markdown("### Scenario + Orchestration Controls")
963
  with gr.Row():
964
  seed_in = gr.Number(value=1337, precision=0, label="Seed")
965
- tick_hours = gr.Slider(0.5, 168.0, value=4.0, step=0.5, label="Simulated hours per tick (speed of time)")
966
- difficulty = gr.Slider(1, 5, value=3, step=1, label="Global difficulty (more friction)")
967
  incident_rate = gr.Slider(0.0, 0.35, value=0.07, step=0.01, label="Incident rate per tick")
968
 
969
  with gr.Row():
@@ -981,46 +773,38 @@ with gr.Blocks(title=TITLE) as demo:
981
  task_est = gr.Slider(0.25, 200.0, value=16.0, step=0.25, label="Estimated hours")
982
  btn_add_task = gr.Button("Add Task")
983
 
984
- gr.Markdown("### Add Agents (unlimited)")
985
  with gr.Row():
986
  agent_name = gr.Textbox(label="Agent name", value="Agent-01")
987
  agent_role = gr.Dropdown(
988
  choices=["Generalist", "Ops", "HR Automation", "Engineer", "Analyst", "Incident Response", "PM"],
989
  value="Engineer",
990
- label="Role"
991
  )
992
  with gr.Row():
993
  model_choice = gr.Dropdown(
994
  choices=["Simulated-Local", "gpt-4o-mini", "gpt-4o", "gpt-5"],
995
  value="Simulated-Local",
996
- label="Model"
997
  )
998
  key_group = gr.Dropdown(
999
  choices=["none", "key1", "key2", "key3"],
1000
  value="none",
1001
- label="Key group (1 key can power up to 10 agents)"
1002
  )
1003
  btn_add_agent = gr.Button("Add Agent")
1004
 
1005
  gr.Markdown("### Model Keys + Pricing (Optional)")
1006
  with gr.Row():
1007
  oai_base = gr.Textbox(label="OpenAI-compatible base URL", value=DEFAULT_OAI_BASE)
1008
- context_prompt = gr.Textbox(
1009
- label="Global Context Prompt (gives the orchestra mission + culture)",
1010
- value="You are simulating a business ops team executing tasks with auditability and cost tracking.",
1011
- lines=3
1012
- )
1013
 
1014
  with gr.Row():
1015
  key1 = gr.Textbox(label="API Key (key1)", type="password")
1016
  key2 = gr.Textbox(label="API Key (key2)", type="password")
1017
  key3 = gr.Textbox(label="API Key (key3)", type="password")
1018
 
1019
- pricing_json = gr.Textbox(
1020
- label="Model pricing JSON (USD per 1M tokens)",
1021
- value=json.dumps(DEFAULT_MODEL_PRICING, indent=2),
1022
- lines=8
1023
- )
1024
  btn_apply_keys = gr.Button("Apply Keys + Pricing")
1025
 
1026
  gr.Markdown("### Autoplay")
@@ -1031,25 +815,8 @@ with gr.Blocks(title=TITLE) as demo:
1031
 
1032
  timer = gr.Timer(value=0.18, active=False)
1033
 
1034
- # -----------------------------
1035
- # Events
1036
- # -----------------------------
1037
- def on_load(w: World, rows: int):
1038
- return (*ui_refresh(w, rows), w)
1039
-
1040
- demo.load(
1041
- on_load,
1042
- inputs=[w_state, runlog_rows],
1043
- outputs=[arena, agents_box, tasks_box, events_box, kpi_box, run_data, w_state],
1044
- queue=True,
1045
- )
1046
-
1047
- def reset_world(seed: int):
1048
- w = init_world(int(seed))
1049
- return w
1050
-
1051
  def do_reset(seed: int, rows: int):
1052
- w = reset_world(seed)
1053
  return (*ui_refresh(w, rows), w)
1054
 
1055
  btn_reset.click(
@@ -1059,13 +826,6 @@ with gr.Blocks(title=TITLE) as demo:
1059
  queue=True,
1060
  )
1061
 
1062
- def apply_scenario(w: World, th: float, diff: int, ir: float):
1063
- w.tick_hours = float(th)
1064
- w.difficulty = int(diff)
1065
- w.incident_rate = float(ir)
1066
- w.events.append(f"[t={w.step}] Scenario updated: tick_hours={w.tick_hours}, difficulty={w.difficulty}, incident_rate={w.incident_rate}")
1067
- return w
1068
-
1069
  def add_task_clicked(w: World, rows: int, title: str, desc: str, p: int, d: int, est: float):
1070
  add_task(w, title, desc, p, d, est)
1071
  return (*ui_refresh(w, rows), w)
@@ -1078,12 +838,10 @@ with gr.Blocks(title=TITLE) as demo:
1078
  )
1079
 
1080
  def add_agent_clicked(w: World, rows: int, name: str, role: str, model: str, kg: str):
1081
- name = (name or "").strip()
1082
- if not name:
1083
- name = f"Agent-{len(w.agents)+1:02d}"
1084
  if name in w.agents:
1085
  name = f"{name}-{len(w.agents)+1}"
1086
- add_agent(w, name=name, model=model, key_group=kg, role=role, seed_bump=len(w.agents)*19)
1087
  return (*ui_refresh(w, rows), w)
1088
 
1089
  btn_add_agent.click(
@@ -1093,32 +851,32 @@ with gr.Blocks(title=TITLE) as demo:
1093
  queue=True,
1094
  )
1095
 
1096
- def apply_keys_pricing(keys_state: Dict[str,str], pricing_state_obj: Dict[str,Any], base_url: str, k1: str, k2: str, k3: str, pricing_txt: str):
1097
- # update keyrings
1098
  keys_state = dict(keys_state) if isinstance(keys_state, dict) else {"none": ""}
1099
  keys_state["none"] = ""
1100
  if k1: keys_state["key1"] = k1.strip()
1101
  if k2: keys_state["key2"] = k2.strip()
1102
  if k3: keys_state["key3"] = k3.strip()
1103
 
1104
- # update pricing
1105
  pj = safe_json(pricing_txt, fallback=None)
1106
  if isinstance(pj, dict):
1107
  pricing_state_obj = pj
1108
 
1109
- # base_url is stored in UI directly; we just return states
1110
  return keys_state, pricing_state_obj
1111
 
1112
  btn_apply_keys.click(
1113
  apply_keys_pricing,
1114
- inputs=[keyrings_state, pricing_state, oai_base, key1, key2, key3, pricing_json],
1115
  outputs=[keyrings_state, pricing_state],
1116
  queue=True,
1117
  )
1118
 
1119
  def run_clicked(w: World, rows: int, n: int, th: float, diff: int, ir: float,
1120
- keys_state: Dict[str,str], pricing_obj: Dict[str,Any], base_url: str, ctx: str):
1121
- w = apply_scenario(w, th, diff, ir)
 
 
 
1122
  n = int(max(1, n))
1123
  r = make_rng(w.seed + w.step * 101)
1124
  for _ in range(n):
@@ -1133,7 +891,7 @@ with gr.Blocks(title=TITLE) as demo:
1133
  )
1134
 
1135
  def download_run_data(w: World, rows: int):
1136
- df = run_data_df(w, rows=50000) # export a lot
1137
  path = to_csv_download(df)
1138
  return path
1139
 
@@ -1144,38 +902,25 @@ with gr.Blocks(title=TITLE) as demo:
1144
  queue=True,
1145
  )
1146
 
1147
- # -----------------------------
1148
- # Autoplay (FIXED — no starred tuple nesting bug)
1149
- # -----------------------------
1150
  def autoplay_start(interval: float):
1151
- interval = float(interval)
1152
- return gr.update(value=interval, active=True), True
1153
 
1154
  def autoplay_stop():
1155
  return gr.update(active=False), False
1156
 
1157
- btn_play.click(
1158
- autoplay_start,
1159
- inputs=[autoplay_speed],
1160
- outputs=[timer, autoplay_on],
1161
- queue=True,
1162
- )
1163
- btn_pause.click(
1164
- autoplay_stop,
1165
- inputs=[],
1166
- outputs=[timer, autoplay_on],
1167
- queue=True,
1168
- )
1169
 
1170
  def autoplay_tick(w: World, is_on: bool, rows: int, th: float, diff: int, ir: float,
1171
- keys_state: Dict[str,str], pricing_obj: Dict[str,Any], base_url: str, ctx: str):
1172
  if not is_on:
1173
  return (*ui_refresh(w, rows), w, is_on, gr.update())
1174
 
1175
- w = apply_scenario(w, th, diff, ir)
 
 
1176
  r = make_rng(w.seed + w.step * 101)
1177
  tick(w, r, pricing_obj, keys_state, base_url, ctx)
1178
-
1179
  return (*ui_refresh(w, rows), w, True, gr.update())
1180
 
1181
  timer.tick(
@@ -1185,4 +930,15 @@ with gr.Blocks(title=TITLE) as demo:
1185
  queue=True,
1186
  )
1187
 
 
 
 
 
 
 
 
 
 
 
 
1188
  demo.queue().launch(ssr_mode=False)
 
2
  import math
3
  import os
4
  import time
 
5
  import random
6
  import tempfile
7
  import urllib.request
 
19
  # ============================================================
20
  # ZEN Orchestrator Sandbox — Business-grade Agent Simulation
21
  # ============================================================
22
+ # Fixes in this regen:
23
+ # - Removes unsupported gr.Dataframe(height=...) for Gradio 5.49.1
24
+ # - Uses a scroll container via HTML/CSS around the dataframe
 
 
 
 
 
 
 
25
  # ============================================================
26
 
 
 
 
 
27
  GRID_W, GRID_H = 32, 20
28
  TILE = 22
29
  HUD_H = 70
 
36
  COL_TEXT = "rgba(235,240,255,0.92)"
37
  COL_TEXT_DIM = "rgba(235,240,255,0.72)"
38
 
 
39
  EMPTY = 0
40
  WALL = 1
41
  DESK = 2
 
44
  INCIDENT = 5
45
  TASK_NODE = 6
46
 
 
 
 
 
 
 
 
 
 
 
47
  TILE_COL = {
48
  EMPTY: "#162044",
49
  WALL: "#cdd2e6",
 
59
  "#ff9b6b", "#c7d2fe", "#a0ffd9", "#ffb0b0",
60
  ]
61
 
 
 
 
 
62
  DEFAULT_MODEL_PRICING = {
 
63
  "Simulated-Local": {"in": 0.00, "out": 0.00},
64
  "gpt-4o-mini": {"in": 0.15, "out": 0.60},
65
  "gpt-4o": {"in": 5.00, "out": 15.00},
66
+ "gpt-5": {"in": 5.00, "out": 15.00},
67
  }
68
 
 
69
  DEFAULT_OAI_BASE = "https://api.openai.com/v1"
70
 
71
 
 
 
 
72
  def clamp(v, lo, hi):
73
  return lo if v < lo else hi if v > hi else v
74
 
 
81
  return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
82
 
83
  def est_tokens(text: str) -> int:
 
84
  if not text:
85
  return 0
86
  return max(1, int(len(text) / 4))
 
97
  return tmp.name
98
 
99
 
 
 
 
100
  @dataclass
101
  class Task:
102
  id: str
103
  title: str
104
  description: str
105
+ priority: int = 3
106
+ difficulty: int = 3
107
  est_hours: float = 8.0
108
  created_step: int = 0
109
+ status: str = "backlog"
110
  assigned_to: Optional[str] = None
111
+ progress: float = 0.0
112
  blockers: List[str] = field(default_factory=list)
113
 
114
  @dataclass
115
  class Agent:
116
  name: str
117
  model: str
118
+ key_group: str
119
  x: int
120
  y: int
121
  energy: float = 100.0
122
  role: str = "Generalist"
123
+ state: str = "idle"
124
  current_task_id: Optional[str] = None
125
  thoughts: str = ""
126
  last_action: str = ""
127
  tokens_in: int = 0
128
  tokens_out: int = 0
129
  cost_usd: float = 0.0
130
+ compute_s: float = 0.0
131
 
132
  @dataclass
133
  class World:
134
  seed: int = 1337
135
  step: int = 0
136
  sim_time_hours: float = 0.0
137
+ tick_hours: float = 4.0
138
+ difficulty: int = 3
139
+ incident_rate: float = 0.07
 
 
 
 
140
  grid: List[List[int]] = field(default_factory=list)
141
  agents: Dict[str, Agent] = field(default_factory=dict)
142
  tasks: Dict[str, Task] = field(default_factory=dict)
 
 
143
  events: List[str] = field(default_factory=list)
144
  runlog: List[Dict[str, Any]] = field(default_factory=list)
 
 
145
  incidents_open: int = 0
146
  incidents_resolved: int = 0
147
  tasks_done: int = 0
 
148
  done: bool = False
149
 
150
 
 
 
 
151
  def build_office(seed: int) -> List[List[int]]:
152
  r = make_rng(seed)
153
  g = [[EMPTY for _ in range(GRID_W)] for _ in range(GRID_H)]
154
 
 
155
  for x in range(GRID_W):
156
  g[0][x] = WALL
157
  g[GRID_H - 1][x] = WALL
 
159
  g[y][0] = WALL
160
  g[y][GRID_W - 1] = WALL
161
 
 
162
  def rect(x0, y0, x1, y1, tile):
163
  for y in range(y0, y1 + 1):
164
  for x in range(x0, x1 + 1):
165
  if 1 <= x < GRID_W - 1 and 1 <= y < GRID_H - 1:
166
  g[y][x] = tile
167
 
 
168
  rect(2, 2, GRID_W - 3, GRID_H - 3, EMPTY)
 
 
169
  rect(3, 3, 10, 7, MEETING)
170
  rect(GRID_W - 11, 3, GRID_W - 4, 7, MEETING)
 
 
171
  rect(GRID_W - 10, GRID_H - 8, GRID_W - 4, GRID_H - 3, SERVER)
172
 
 
173
  for y in range(9, GRID_H - 10):
174
  for x in range(4, GRID_W - 12):
175
  if (x % 3 == 1) and (y % 2 == 0):
176
  g[y][x] = DESK
177
 
 
178
  nodes = [(6, GRID_H - 5), (GRID_W // 2, GRID_H // 2), (GRID_W - 14, 10)]
179
  for (x, y) in nodes:
180
  if 1 <= x < GRID_W - 1 and 1 <= y < GRID_H - 1:
181
  g[y][x] = TASK_NODE
182
 
 
183
  for _ in range(22):
184
  x = r.randint(3, GRID_W - 4)
185
  y = r.randint(8, GRID_H - 9)
 
196
  opts.append((x, y))
197
  return r.choice(opts) if opts else (2, 2)
198
 
 
 
 
 
199
  def init_world(seed: int) -> World:
200
  seed = int(seed)
201
  g = build_office(seed)
 
206
  def add_agent(w: World, name: str, model: str, key_group: str, role: str, seed_bump: int = 0):
207
  r = make_rng(w.seed + w.step * 17 + seed_bump)
208
  x, y = random_walkable_cell(w.grid, r)
209
+ w.agents[name] = Agent(name=name, model=model, key_group=key_group, x=x, y=y, role=role)
 
 
 
 
 
 
210
  w.events.append(f"[t={w.step}] Agent added: {name} | model={model} | key_group={key_group} | role={role}")
211
 
212
  def add_task(w: World, title: str, description: str, priority: int, difficulty: int, est_hours: float):
 
224
  return tid
225
 
226
 
 
 
 
227
  DIRS4 = [(1,0), (0,1), (-1,0), (0,-1)]
228
 
229
  def in_bounds(x, y):
 
260
  return cur
261
 
262
 
 
 
 
 
263
  def oai_chat_completion(base_url: str, api_key: str, model: str, messages: List[Dict[str,str]], timeout_s: int = 25) -> Dict[str, Any]:
264
  url = base_url.rstrip("/") + "/chat/completions"
265
  payload = json.dumps({
 
271
  req = urllib.request.Request(
272
  url,
273
  data=payload,
274
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
 
 
 
275
  method="POST",
276
  )
277
  try:
 
287
  except Exception as e:
288
  return {"error": {"message": str(e)}}
289
 
 
 
 
 
 
 
290
  def price_for(model_pricing: Dict[str, Dict[str,float]], model: str, tokens_in: int, tokens_out: int) -> float:
291
  p = model_pricing.get(model) or model_pricing.get("Simulated-Local") or {"in":0.0, "out":0.0}
292
  return (tokens_in / 1_000_000.0) * float(p.get("in", 0.0)) + (tokens_out / 1_000_000.0) * float(p.get("out", 0.0))
293
 
294
 
 
 
 
295
  def choose_task_for_agent(w: World, agent: Agent) -> Optional[str]:
 
296
  backlog = [t for t in w.tasks.values() if t.status in ("backlog", "blocked")]
297
  if not backlog:
298
  return None
 
300
  return backlog[0].id
301
 
302
  def maybe_generate_incident(w: World, r: random.Random):
 
303
  rate = w.incident_rate * (0.6 + 0.25 * w.difficulty)
304
  if r.random() < rate:
 
305
  x, y = random_walkable_cell(w.grid, r)
306
  if w.grid[y][x] != WALL:
307
  w.grid[y][x] = INCIDENT
308
  w.incidents_open += 1
309
  w.events.append(f"[t={w.step}] INCIDENT spawned at ({x},{y})")
 
310
  add_task(
311
  w,
312
  title="Handle incident",
 
336
  return nodes[0]
337
 
338
 
 
 
 
339
  def simulated_reasoning(agent: Agent, task: Task, w: World) -> Tuple[str, str, int, int, float]:
 
 
 
 
340
  base = 0.08 + 0.04 * w.difficulty + 0.03 * task.difficulty
341
  compute_s = clamp(base, 0.05, 0.6)
 
 
342
  thoughts = (
343
+ f"Assess '{task.title}'. Priority={task.priority} difficulty={task.difficulty}. "
344
+ f"Plan: decompose, execute, verify, document."
 
 
 
345
  )
346
+ action = f"Progressed {task.id}. Updated notes, checked blockers, validated output."
347
  tin = est_tokens(task.title + " " + task.description) + 30
348
  tout = est_tokens(thoughts + " " + action) + 40
349
  return thoughts, action, tin, tout, compute_s
350
 
351
+ def api_reasoning(agent: Agent, task: Task, w: World, base_url: str, api_key: str, model: str, context_prompt: str):
 
 
 
 
 
 
 
 
 
 
 
352
  t0 = time.time()
 
353
  sys = (
354
  "You are an autonomous business operations agent in a multi-agent simulation. "
355
  "Return a JSON object with keys: thoughts, action, blockers (list), progress_delta (0..1). "
356
  "Keep thoughts short and action concrete."
357
  )
358
  user = {
359
+ "simulation": {"step": w.step, "sim_time_hours": w.sim_time_hours, "difficulty": w.difficulty, "incidents_open": w.incidents_open},
360
+ "agent": {"name": agent.name, "role": agent.role, "energy": agent.energy},
 
 
 
 
 
 
 
 
 
361
  "task": asdict(task),
362
+ "context": (context_prompt or "")[:1400],
363
  }
364
+ resp = oai_chat_completion(base_url, api_key, model, [{"role":"system","content":sys},{"role":"user","content":json.dumps(user)}])
 
 
 
 
 
 
 
 
 
365
  compute_s = float(time.time() - t0)
366
 
367
  if "error" in resp:
 
381
 
382
  obj = safe_json(content, fallback=None)
383
  if not isinstance(obj, dict):
 
384
  thoughts = "Provider returned non-JSON; using fallback."
385
  action = content[:400]
386
  tin = usage_in if isinstance(usage_in, int) else est_tokens(sys + json.dumps(user))
 
392
  blockers = obj.get("blockers", [])
393
  progress_delta = obj.get("progress_delta", 0.0)
394
 
 
395
  if isinstance(blockers, list) and blockers:
396
  task.blockers = [str(b)[:80] for b in blockers][:5]
397
  task.status = "blocked"
 
401
  task.status = "in_progress"
402
 
403
  try:
404
+ task.progress = clamp(task.progress + float(progress_delta), 0.0, 1.0)
 
405
  except Exception:
406
  pass
407
 
 
410
  return thoughts, action, tin, tout, compute_s, None
411
 
412
 
413
+ def step_agent(w: World, agent: Agent, r: random.Random, model_pricing: Dict[str, Dict[str, float]], keyrings: Dict[str, str], base_url: str, context_prompt: str):
 
 
 
 
 
 
 
 
 
 
 
414
  if agent.energy <= 0:
415
  agent.state = "blocked"
416
  agent.last_action = "Out of energy"
417
  return
418
 
 
419
  if agent.current_task_id is None or agent.current_task_id not in w.tasks:
420
  tid = choose_task_for_agent(w, agent)
421
  if tid is None:
 
430
 
431
  task = w.tasks[agent.current_task_id]
432
 
 
433
  incs = incident_positions(w)
 
434
  if incs and task.priority >= 5:
435
+ incs.sort(key=lambda p: abs(p[0]-agent.x)+abs(p[1]-agent.y))
436
  target = incs[0]
437
  else:
438
  target = nearest_task_node(w, agent.x, agent.y)
 
445
  agent.energy = max(0.0, agent.energy - 0.8)
446
  return
447
 
 
448
  agent.state = "working"
449
 
450
  if agent.model == "Simulated-Local" or agent.key_group == "none":
451
  thoughts, action, tin, tout, compute_s = simulated_reasoning(agent, task, w)
452
  err = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
  speed = 0.08 / (0.6 + 0.25 * w.difficulty + 0.30 * task.difficulty)
454
  task.progress = clamp(task.progress + speed, 0.0, 1.0)
455
  if task.progress < 1.0:
456
  task.status = "in_progress"
457
+ else:
458
+ key = (keyrings or {}).get(agent.key_group, "")
459
+ if not key:
460
+ thoughts, action, tin, tout, compute_s = simulated_reasoning(agent, task, w)
461
+ err = f"No key for group '{agent.key_group}', used local simulation."
462
+ speed = 0.08 / (0.6 + 0.25 * w.difficulty + 0.30 * task.difficulty)
463
+ task.progress = clamp(task.progress + speed, 0.0, 1.0)
464
+ else:
465
+ thoughts, action, tin, tout, compute_s, err = api_reasoning(agent, task, w, base_url, key, agent.model, context_prompt)
466
 
 
467
  if task.progress >= 1.0 and task.status != "done":
468
  task.status = "done"
469
  w.tasks_done += 1
470
  w.events.append(f"[t={w.step}] DONE {task.id}: {task.title}")
471
  agent.current_task_id = None
472
 
 
473
  if "incident" in task.title.lower():
 
474
  incs = incident_positions(w)
475
  if incs:
476
  x, y = incs[0]
 
479
  w.incidents_resolved += 1
480
  w.events.append(f"[t={w.step}] Incident resolved at ({x},{y})")
481
 
 
482
  agent.thoughts = thoughts
483
  agent.last_action = action if action else agent.last_action
484
  agent.tokens_in += int(tin)
485
  agent.tokens_out += int(tout)
486
  agent.compute_s += float(compute_s)
487
  agent.cost_usd += price_for(model_pricing, agent.model, int(tin), int(tout))
 
 
488
  agent.energy = max(0.0, agent.energy - (0.8 + 0.15 * w.difficulty + 0.12 * task.difficulty))
489
 
 
490
  w.runlog.append({
491
  "step": w.step,
492
  "sim_time_hours": round(w.sim_time_hours, 2),
 
507
  })
508
 
509
 
510
+ def tick(w: World, r: random.Random, model_pricing: Dict[str, Dict[str, float]], keyrings: Dict[str, str], base_url: str, context_prompt: str, max_log: int = 4000):
 
 
 
 
 
 
 
 
511
  if w.done:
512
  return
 
 
513
  maybe_generate_incident(w, r)
 
 
514
  agents = list(w.agents.values())
515
  agents.sort(key=lambda a: (a.energy, a.name))
 
516
  for ag in agents:
517
  step_agent(w, ag, r, model_pricing, keyrings, base_url, context_prompt)
 
 
518
  w.step += 1
519
  w.sim_time_hours += float(w.tick_hours)
 
 
520
  if len(w.events) > 250:
521
  w.events = w.events[-250:]
522
  if len(w.runlog) > max_log:
523
  w.runlog = w.runlog[-max_log:]
524
 
525
 
 
 
 
526
  def compute_kpis(w: World) -> Dict[str, Any]:
527
  backlog = sum(1 for t in w.tasks.values() if t.status == "backlog")
528
  inprog = sum(1 for t in w.tasks.values() if t.status == "in_progress")
529
  blocked = sum(1 for t in w.tasks.values() if t.status == "blocked")
530
  done = sum(1 for t in w.tasks.values() if t.status == "done")
 
531
  total_cost = sum(a.cost_usd for a in w.agents.values())
532
  total_tokens_in = sum(a.tokens_in for a in w.agents.values())
533
  total_tokens_out = sum(a.tokens_out for a in w.agents.values())
534
  total_compute = sum(a.compute_s for a in w.agents.values())
 
 
535
  days = max(1e-6, w.sim_time_hours / 24.0)
536
  tpd = done / days
 
537
  return {
538
  "sim_time_days": round(w.sim_time_hours / 24.0, 2),
539
  "agents": len(w.agents),
 
552
  }
553
 
554
 
 
 
 
555
  def svg_render(w: World) -> str:
556
  k = compute_kpis(w)
 
557
  headline = (
558
  f"ZEN Orchestrator Sandbox • step={w.step} • sim_days={k['sim_time_days']} • "
559
  f"agents={k['agents']} • done={k['tasks_done']} • backlog={k['tasks_backlog']} • "
560
  f"incidents_open={k['incidents_open']} • cost=${k['cost_usd_total']}"
561
  )
562
+ detail = f"tick_hours={w.tick_hours} • difficulty={w.difficulty} • incident_rate={round(w.incident_rate,3)}"
 
 
563
 
564
  css = f"""
565
  <style>
 
593
  50% {{ transform: scale(1.15); opacity: 0.26; }}
594
  100% {{ transform: scale(1.0); opacity: 0.14; }}
595
  }}
596
+ .tag {{
597
+ fill: rgba(0,0,0,0.38);
 
 
598
  }}
599
  .tile {{
600
  shape-rendering: crispEdges;
601
  }}
 
 
 
602
  </style>
603
  """
604
 
 
612
  <text class="hud hudSmall" x="16" y="52" font-size="12">{detail}</text>
613
  """]
614
 
 
615
  for y in range(GRID_H):
616
  for x in range(GRID_W):
617
  t = w.grid[y][x]
 
619
  px = x * TILE
620
  py = HUD_H + y * TILE
621
  parts.append(f'<rect class="tile" x="{px}" y="{py}" width="{TILE}" height="{TILE}" fill="{col}"/>')
 
 
 
 
 
 
622
  if t == INCIDENT:
623
  parts.append(f'<circle cx="{px+TILE/2}" cy="{py+TILE/2}" r="7" fill="rgba(0,0,0,0.25)"/>')
624
  parts.append(f'<circle cx="{px+TILE/2}" cy="{py+TILE/2}" r="4" fill="rgba(255,255,255,0.65)"/>')
625
 
 
626
  for x in range(GRID_W + 1):
627
  px = x * TILE
628
  parts.append(f'<line class="gridline" x1="{px}" y1="{HUD_H}" x2="{px}" y2="{SVG_H}"/>')
 
630
  py = HUD_H + y * TILE
631
  parts.append(f'<line class="gridline" x1="0" y1="{py}" x2="{SVG_W}" y2="{py}"/>')
632
 
 
633
  for i, ag in enumerate(w.agents.values()):
634
  col = AGENT_COLORS[i % len(AGENT_COLORS)]
635
  px = ag.x * TILE
 
638
  <g class="agent" style="transform: translate({px}px, {py}px);">
639
  <circle class="pulse" cx="{TILE/2}" cy="{TILE/2}" r="{TILE*0.48}" fill="{col}"></circle>
640
  <circle cx="{TILE/2}" cy="{TILE/2}" r="{TILE*0.34}" fill="{col}" opacity="0.98"></circle>
641
+ <rect class="tag" x="{TILE*0.10}" y="{TILE*0.08}" width="{TILE*0.80}" height="14" rx="7"/>
642
+ <text x="{TILE/2}" y="{TILE*0.08 + 11}" text-anchor="middle" font-size="9"
643
+ fill="rgba(235,240,255,0.90)" font-family="ui-sans-serif, system-ui">{ag.name}</text>
644
  """)
 
 
 
 
 
 
645
  bar_w = TILE * 0.80
646
  bx = TILE/2 - bar_w/2
647
  by = TILE * 0.82
648
  parts.append(f'<rect x="{bx}" y="{by}" width="{bar_w}" height="6" rx="4" fill="rgba(255,255,255,0.12)"/>')
649
  parts.append(f'<rect x="{bx}" y="{by}" width="{bar_w*(clamp(ag.energy,0,100)/100.0)}" height="6" rx="4" fill="rgba(122,255,200,0.85)"/>')
 
650
  parts.append("</g>")
651
 
652
  parts.append("</svg></div>")
653
  return "".join(parts)
654
 
655
 
 
 
 
656
  def agents_text(w: World) -> str:
657
  lines = []
658
  for ag in w.agents.values():
 
665
  return "\n".join(lines) if lines else "(no agents yet)"
666
 
667
  def tasks_text(w: World) -> str:
 
668
  tasks = list(w.tasks.values())
669
  tasks.sort(key=lambda t: (t.status != "done", -t.priority, t.created_step))
670
  out = []
 
680
  return "\n".join(w.events[-20:]) if w.events else ""
681
 
682
  def kpis_text(w: World) -> str:
683
+ return json.dumps(compute_kpis(w), indent=2)
 
684
 
685
  def run_data_df(w: World, rows: int) -> pd.DataFrame:
686
  rows = int(max(10, rows))
 
689
  "step","sim_time_hours","agent","role","model","key_group","task_id","task_status","task_progress",
690
  "action","thoughts","tokens_in","tokens_out","cost_usd","compute_s","error"
691
  ])
692
+ return pd.DataFrame(w.runlog[-rows:])
 
693
 
694
  def ui_refresh(w: World, run_rows: int):
695
+ return (
696
+ svg_render(w),
697
+ agents_text(w),
698
+ tasks_text(w),
699
+ events_text(w),
700
+ kpis_text(w),
701
+ run_data_df(w, run_rows),
702
+ )
703
+
704
+
 
 
705
  TITLE = "ZEN Orchestrator Sandbox — Business-grade Agent Orchestra Simulator"
706
 
707
  with gr.Blocks(title=TITLE) as demo:
708
  gr.Markdown(
709
  f"## {TITLE}\n"
710
+ "Business-oriented multi-agent simulation with game-like visuals, time controls, run logging, and optional model keys."
 
711
  )
712
 
713
+ # CSS scroll wrapper for the dataframe (Gradio 5.49.1-safe)
714
+ gr.HTML("""
715
+ <style>
716
+ .zen-scroll {
717
+ max-height: 320px;
718
+ overflow: auto;
719
+ border-radius: 12px;
720
+ border: 1px solid rgba(255,255,255,0.10);
721
+ background: rgba(255,255,255,0.03);
722
+ padding: 10px;
723
+ }
724
+ </style>
725
+ """)
726
+
727
  w_state = gr.State(init_world(1337))
728
  autoplay_on = gr.State(False)
 
 
729
  keyrings_state = gr.State({"none": ""})
730
  pricing_state = gr.State(DEFAULT_MODEL_PRICING)
731
 
 
743
  with gr.Row():
744
  runlog_rows = gr.Slider(50, 1500, value=250, step=50, label="Run Data rows to display")
745
  download_btn = gr.Button("Download Run Data CSV")
746
+
747
+ # Scroll wrapper around dataframe
748
+ gr.HTML('<div class="zen-scroll">')
749
+ run_data = gr.Dataframe(label="Run Data", interactive=False, wrap=True) # ✅ no height kwarg
750
+ gr.HTML('</div>')
751
+
752
  download_file = gr.File(label="CSV Download", interactive=False)
753
 
754
  gr.Markdown("### Scenario + Orchestration Controls")
755
  with gr.Row():
756
  seed_in = gr.Number(value=1337, precision=0, label="Seed")
757
+ tick_hours = gr.Slider(0.5, 168.0, value=4.0, step=0.5, label="Simulated hours per tick")
758
+ difficulty = gr.Slider(1, 5, value=3, step=1, label="Global difficulty")
759
  incident_rate = gr.Slider(0.0, 0.35, value=0.07, step=0.01, label="Incident rate per tick")
760
 
761
  with gr.Row():
 
773
  task_est = gr.Slider(0.25, 200.0, value=16.0, step=0.25, label="Estimated hours")
774
  btn_add_task = gr.Button("Add Task")
775
 
776
+ gr.Markdown("### Add Agents")
777
  with gr.Row():
778
  agent_name = gr.Textbox(label="Agent name", value="Agent-01")
779
  agent_role = gr.Dropdown(
780
  choices=["Generalist", "Ops", "HR Automation", "Engineer", "Analyst", "Incident Response", "PM"],
781
  value="Engineer",
782
+ label="Role",
783
  )
784
  with gr.Row():
785
  model_choice = gr.Dropdown(
786
  choices=["Simulated-Local", "gpt-4o-mini", "gpt-4o", "gpt-5"],
787
  value="Simulated-Local",
788
+ label="Model",
789
  )
790
  key_group = gr.Dropdown(
791
  choices=["none", "key1", "key2", "key3"],
792
  value="none",
793
+ label="Key group",
794
  )
795
  btn_add_agent = gr.Button("Add Agent")
796
 
797
  gr.Markdown("### Model Keys + Pricing (Optional)")
798
  with gr.Row():
799
  oai_base = gr.Textbox(label="OpenAI-compatible base URL", value=DEFAULT_OAI_BASE)
800
+ context_prompt = gr.Textbox(label="Global Context Prompt", value="Simulate a business ops team with auditability and cost tracking.", lines=3)
 
 
 
 
801
 
802
  with gr.Row():
803
  key1 = gr.Textbox(label="API Key (key1)", type="password")
804
  key2 = gr.Textbox(label="API Key (key2)", type="password")
805
  key3 = gr.Textbox(label="API Key (key3)", type="password")
806
 
807
+ pricing_json = gr.Textbox(label="Model pricing JSON (USD per 1M tokens)", value=json.dumps(DEFAULT_MODEL_PRICING, indent=2), lines=8)
 
 
 
 
808
  btn_apply_keys = gr.Button("Apply Keys + Pricing")
809
 
810
  gr.Markdown("### Autoplay")
 
815
 
816
  timer = gr.Timer(value=0.18, active=False)
817
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  def do_reset(seed: int, rows: int):
819
+ w = init_world(int(seed))
820
  return (*ui_refresh(w, rows), w)
821
 
822
  btn_reset.click(
 
826
  queue=True,
827
  )
828
 
 
 
 
 
 
 
 
829
  def add_task_clicked(w: World, rows: int, title: str, desc: str, p: int, d: int, est: float):
830
  add_task(w, title, desc, p, d, est)
831
  return (*ui_refresh(w, rows), w)
 
838
  )
839
 
840
  def add_agent_clicked(w: World, rows: int, name: str, role: str, model: str, kg: str):
841
+ name = (name or "").strip() or f"Agent-{len(w.agents)+1:02d}"
 
 
842
  if name in w.agents:
843
  name = f"{name}-{len(w.agents)+1}"
844
+ add_agent(w, name=name, model=model, key_group=kg, role=role, seed_bump=len(w.agents) * 19)
845
  return (*ui_refresh(w, rows), w)
846
 
847
  btn_add_agent.click(
 
851
  queue=True,
852
  )
853
 
854
+ def apply_keys_pricing(keys_state: Dict[str, str], pricing_state_obj: Dict[str, Any], k1: str, k2: str, k3: str, pricing_txt: str):
 
855
  keys_state = dict(keys_state) if isinstance(keys_state, dict) else {"none": ""}
856
  keys_state["none"] = ""
857
  if k1: keys_state["key1"] = k1.strip()
858
  if k2: keys_state["key2"] = k2.strip()
859
  if k3: keys_state["key3"] = k3.strip()
860
 
 
861
  pj = safe_json(pricing_txt, fallback=None)
862
  if isinstance(pj, dict):
863
  pricing_state_obj = pj
864
 
 
865
  return keys_state, pricing_state_obj
866
 
867
  btn_apply_keys.click(
868
  apply_keys_pricing,
869
+ inputs=[keyrings_state, pricing_state, key1, key2, key3, pricing_json],
870
  outputs=[keyrings_state, pricing_state],
871
  queue=True,
872
  )
873
 
874
  def run_clicked(w: World, rows: int, n: int, th: float, diff: int, ir: float,
875
+ keys_state: Dict[str, str], pricing_obj: Dict[str, Any], base_url: str, ctx: str):
876
+ w.tick_hours = float(th)
877
+ w.difficulty = int(diff)
878
+ w.incident_rate = float(ir)
879
+ w.events.append(f"[t={w.step}] Scenario updated: tick_hours={w.tick_hours}, difficulty={w.difficulty}, incident_rate={w.incident_rate}")
880
  n = int(max(1, n))
881
  r = make_rng(w.seed + w.step * 101)
882
  for _ in range(n):
 
891
  )
892
 
893
  def download_run_data(w: World, rows: int):
894
+ df = run_data_df(w, rows=50000)
895
  path = to_csv_download(df)
896
  return path
897
 
 
902
  queue=True,
903
  )
904
 
 
 
 
905
  def autoplay_start(interval: float):
906
+ return gr.update(value=float(interval), active=True), True
 
907
 
908
  def autoplay_stop():
909
  return gr.update(active=False), False
910
 
911
+ btn_play.click(autoplay_start, inputs=[autoplay_speed], outputs=[timer, autoplay_on], queue=True)
912
+ btn_pause.click(autoplay_stop, inputs=[], outputs=[timer, autoplay_on], queue=True)
 
 
 
 
 
 
 
 
 
 
913
 
914
  def autoplay_tick(w: World, is_on: bool, rows: int, th: float, diff: int, ir: float,
915
+ keys_state: Dict[str, str], pricing_obj: Dict[str, Any], base_url: str, ctx: str):
916
  if not is_on:
917
  return (*ui_refresh(w, rows), w, is_on, gr.update())
918
 
919
+ w.tick_hours = float(th)
920
+ w.difficulty = int(diff)
921
+ w.incident_rate = float(ir)
922
  r = make_rng(w.seed + w.step * 101)
923
  tick(w, r, pricing_obj, keys_state, base_url, ctx)
 
924
  return (*ui_refresh(w, rows), w, True, gr.update())
925
 
926
  timer.tick(
 
930
  queue=True,
931
  )
932
 
933
+ # initial render
934
+ def on_load(w: World, rows: int):
935
+ return (*ui_refresh(w, rows), w)
936
+
937
+ demo.load(
938
+ on_load,
939
+ inputs=[w_state, runlog_rows],
940
+ outputs=[arena, agents_box, tasks_box, events_box, kpi_box, run_data, w_state],
941
+ queue=True,
942
+ )
943
+
944
  demo.queue().launch(ssr_mode=False)