Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
#
|
| 24 |
-
# -
|
| 25 |
-
# -
|
| 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},
|
| 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
|
| 142 |
-
difficulty: int = 3
|
| 143 |
est_hours: float = 8.0
|
| 144 |
created_step: int = 0
|
| 145 |
-
status: str = "backlog"
|
| 146 |
assigned_to: Optional[str] = None
|
| 147 |
-
progress: float = 0.0
|
| 148 |
blockers: List[str] = field(default_factory=list)
|
| 149 |
|
| 150 |
@dataclass
|
| 151 |
class Agent:
|
| 152 |
name: str
|
| 153 |
model: str
|
| 154 |
-
key_group: str
|
| 155 |
x: int
|
| 156 |
y: int
|
| 157 |
energy: float = 100.0
|
| 158 |
role: str = "Generalist"
|
| 159 |
-
state: str = "idle"
|
| 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
|
| 167 |
|
| 168 |
@dataclass
|
| 169 |
class World:
|
| 170 |
seed: int = 1337
|
| 171 |
step: int = 0
|
| 172 |
sim_time_hours: float = 0.0
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 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"
|
| 444 |
-
f"Plan:
|
| 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 |
-
|
| 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]
|
| 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 |
-
|
| 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)
|
| 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 |
-
.
|
| 795 |
-
fill: rgba(
|
| 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 |
-
|
| 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 |
-
|
| 914 |
-
return pd.DataFrame(data)
|
| 915 |
|
| 916 |
def ui_refresh(w: World, run_rows: int):
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 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 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 966 |
-
difficulty = gr.Slider(1, 5, value=3, step=1, label="Global difficulty
|
| 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
|
| 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
|
| 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 =
|
| 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],
|
| 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,
|
| 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 =
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 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 |
-
|
| 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 |
-
|
| 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 =
|
|
|
|
|
|
|
| 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)
|