Add Command Center v2: DQ/RAG integration, S3 browser, Jupyter, GPU matrix.

Mirror mo/atc-GPU layout with config/, docs/, scripts/ for Gitea deploy.
This commit is contained in:
mo
2026-06-25 00:28:23 +00:00
parent fb9cc21c9a
commit a11621b21f
110 changed files with 14622 additions and 529 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
COPY main.py lab_context.py agent_terminal.py workload.py node_registry.py node_ops.py topology_views.py supervisor.py approval_service.py db.py dockhand_envs.py presentation.py database_inventory.py presentation_upload.py presentation_static.py storage_s3.py .
RUN mkdir -p /data
ENV DATABASE_URL=sqlite:////data/atc-agents.db
EXPOSE 3201
+72
View File
@@ -0,0 +1,72 @@
"""Per-agent live terminal buffers and streaming."""
from __future__ import annotations
import uuid
from collections import deque
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable
MAX_LINES_PER_AGENT = 300
PublishFn = Callable[[dict[str, Any]], Awaitable[None]]
_buffers: dict[str, deque[dict[str, Any]]] = {}
_publish: PublishFn | None = None
def init_terminals(agent_ids: list[str]) -> None:
for aid in agent_ids:
if aid not in _buffers:
_buffers[aid] = deque(maxlen=MAX_LINES_PER_AGENT)
def set_terminal_publisher(fn: PublishFn) -> None:
global _publish
_publish = fn
def get_terminal_lines(agent_id: str, limit: int = 200) -> list[dict[str, Any]]:
buf = _buffers.get(agent_id, deque())
items = list(buf)
return items[-limit:]
def get_all_terminals(limit: int = 200) -> dict[str, list[dict[str, Any]]]:
return {aid: get_terminal_lines(aid, limit) for aid in _buffers}
async def terminal_log(
agent_id: str,
text: str,
*,
level: str = "info",
phase: str = "ops",
prompt_id: str | None = None,
mirror: bool = True,
) -> dict[str, Any]:
init_terminals([agent_id])
line = {
"id": str(uuid.uuid4())[:8],
"ts": datetime.now(timezone.utc).isoformat(),
"agent_id": agent_id,
"level": level,
"phase": phase,
"text": text,
"prompt_id": prompt_id,
}
_buffers[agent_id].append(line)
if _publish and mirror:
await _publish({"type": "terminal", "line": line})
return line
# Type: async (level, phase, text) -> None
TerminalLogFn = Callable[[str, str, str], Awaitable[None]]
def make_logger(agent_id: str, prompt_id: str | None = None) -> TerminalLogFn:
async def log(level: str, phase: str, text: str) -> None:
await terminal_log(agent_id, text, level=level, phase=phase, prompt_id=prompt_id)
return log
+248
View File
@@ -0,0 +1,248 @@
"""Approval workflow — agents request; Mo & Bart approve before mutating actions."""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from typing import Any, Awaitable, Callable
from sqlalchemy import select
from sqlalchemy.orm import Session
APPROVAL_ACTION_TYPES = {
"docker.restart": "Docker container restart",
"docker.update": "Docker image update / pull",
"docker.recreate": "Docker container recreate",
"docker.stop": "Docker container stop",
"docker.remove": "Docker container remove",
"docker.compose": "Docker Compose deploy",
"docker.prune": "Docker prune / cleanup",
"db.migrate": "Database schema migration",
"db.restart": "Database service restart",
"etl.restart": "ETL / connector restart",
"kafka.reset": "Kafka topic / offset reset",
"hdfs.mutate": "HDFS destructive operation",
"infra.reboot": "VM / host reboot",
"generic.mutate": "Infrastructure change",
}
SUPERVISOR_IDS = ["mo-commander", "bart-commander"]
_INTENT_PATTERNS: list[tuple[re.Pattern[str], str]] = [
(re.compile(r"\b(restart|herstart|reboot)\b.*\b(container|docker|stack|service|mysql|postgres|kafka|airflow)"), "docker.restart"),
(re.compile(r"\b(update|upgrade|updaten|pull|pullen)\b.*\b(docker|image|container|stack|compose)"), "docker.update"),
(re.compile(r"\b(recreate|rebuild|redeploy|deploy|opnieuw)\b.*\b(container|docker|stack|compose|service)"), "docker.recreate"),
(re.compile(r"\b(stop|stoppen|shutdown)\b.*\b(container|docker|service)"), "docker.stop"),
(re.compile(r"\b(remove|delete|verwijder|rm|prune|opschonen)\b.*\b(container|docker|image|volume)"), "docker.remove"),
(re.compile(r"\b(docker compose|compose up|stack deploy)"), "docker.compose"),
(re.compile(r"\b(migrate|migration|schema change)\b.*\b(db|database|postgres|mysql)"), "db.migrate"),
(re.compile(r"\b(restart|herstart)\b.*\b(db|database|postgres|mysql|mongo|cassandra)"), "db.restart"),
(re.compile(r"\b(restart|reset)\b.*\b(connector|debezium|kafka connect)"), "etl.restart"),
(re.compile(r"\b(reset|truncate|drop)\b.*\b(topic|kafka|offset)"), "kafka.reset"),
(re.compile(r"\b(reboot|restart)\b.*\b(vm|host|server|node|proxmox)"), "infra.reboot"),
]
_RESPONSE_ACTION_PATTERN = re.compile(
r"\b(will|ga|moet|plan to|going to|propose|voorstel)\b.*\b(restart|update|pull|recreate|deploy|stop|remove|reboot|migrate)",
re.I,
)
def detect_approval_intent(text: str) -> dict[str, Any] | None:
lower = text.lower().strip()
for pattern, action_type in _INTENT_PATTERNS:
if pattern.search(lower):
return {
"action_type": action_type,
"action": text.strip()[:500],
"reason": f"Mutating operation detected ({APPROVAL_ACTION_TYPES.get(action_type, action_type)})",
}
return None
def detect_agent_proposed_action(llm_answer: str, original_message: str) -> dict[str, Any] | None:
if not _RESPONSE_ACTION_PATTERN.search(llm_answer):
return None
intent = detect_approval_intent(llm_answer) or detect_approval_intent(original_message)
if intent:
intent["reason"] = f"Agent proposed action in mission response: {intent['reason']}"
intent["action"] = llm_answer.strip()[:500]
return intent
def approval_to_dict(row: Any) -> dict[str, Any]:
payload: dict[str, Any] = {}
raw_payload = getattr(row, "payload", None)
if raw_payload:
try:
payload = json.loads(raw_payload)
except (json.JSONDecodeError, TypeError):
payload = {"raw": raw_payload}
decided_at = getattr(row, "decided_at", None)
return {
"id": row.id,
"ts": row.ts.isoformat() if row.ts else None,
"agent_id": row.agent_id,
"action": row.action,
"reason": row.reason,
"status": row.status,
"action_type": getattr(row, "action_type", None) or "generic.mutate",
"target": getattr(row, "target", None) or "",
"payload": payload,
"decided_by": getattr(row, "decided_by", None),
"decide_note": getattr(row, "decide_note", None),
"decided_at": decided_at.isoformat() if decided_at else None,
"priority": getattr(row, "priority", None) or "normal",
}
def migrate_approval_columns(engine: Any) -> None:
"""Legacy shim — migrations live in db.py."""
from db import migrate_approval_columns as _migrate
_migrate(engine)
async def create_approval_request(
*,
db: Session,
ApprovalModel: type,
agent_id: str,
action: str,
reason: str,
action_type: str = "generic.mutate",
target: str = "",
payload: dict | None = None,
priority: str = "normal",
terminal_log: Callable[..., Awaitable[None]] | None = None,
mirror_supervisors: Callable[..., Awaitable[None]] | None = None,
publish: Callable[[dict], Awaitable[None]] | None = None,
add_feed: Callable[[str, str, str], dict] | None = None,
) -> dict[str, Any]:
import uuid
approval_id = str(uuid.uuid4())[:10]
now = datetime.now(timezone.utc)
row = ApprovalModel(
id=approval_id,
ts=now,
agent_id=agent_id,
action=action,
reason=reason,
status="pending",
action_type=action_type,
target=target,
payload=json.dumps(payload or {}),
priority=priority,
)
db.add(row)
db.commit()
db.refresh(row)
item = approval_to_dict(row)
type_label = APPROVAL_ACTION_TYPES.get(action_type, action_type)
alert = (
f"⚠ APPROVAL REQUIRED · {type_label}\n"
f" Agent: {agent_id}\n"
f" Action: {action[:200]}\n"
f" Target: {target or ''}\n"
f" Reason: {reason[:200]}\n"
f" ID: {approval_id} — awaiting Mo & Bart"
)
if terminal_log:
for sid in SUPERVISOR_IDS:
await terminal_log(sid, alert, level="warn", phase="approval")
await terminal_log(
agent_id,
f"⏸ Action queued for approval ({approval_id}) — Mo & Bart notified",
level="warn",
phase="approval",
)
if mirror_supervisors:
await mirror_supervisors(
agent_id,
f"APPROVAL REQUEST [{approval_id}] {action_type}: {action[:120]}",
level="warn",
phase="approval",
)
if add_feed and publish:
feed = add_feed(
agent_id,
f"Approval requested ({approval_id}): {action[:80]} — waiting for Mo & Bart",
"warn",
)
await publish({"type": "feed", "entry": feed})
if publish:
await publish({"type": "approval_new", "approval": item})
return item
async def decide_approval_request(
*,
db: Session,
ApprovalModel: type,
approval_id: str,
approved: bool,
decided_by: str = "mo-commander",
note: str = "",
terminal_log: Callable[..., Awaitable[None]] | None = None,
publish: Callable[[dict], Awaitable[None]] | None = None,
add_feed: Callable[[str, str, str], dict] | None = None,
) -> dict[str, Any] | None:
row = db.get(ApprovalModel, approval_id)
if not row:
return None
if row.status != "pending":
return approval_to_dict(row)
row.status = "approved" if approved else "denied"
row.decided_by = decided_by
row.decide_note = note or None
row.decided_at = datetime.now(timezone.utc)
db.commit()
db.refresh(row)
item = approval_to_dict(row)
verb = "APPROVED" if approved else "DENIED"
who = "Mo" if "mo" in decided_by else "Bart" if "bart" in decided_by else decided_by
msg = f"{verb} by {who}: {row.action[:100]}"
if note:
msg += f"{note[:80]}"
if terminal_log:
await terminal_log(row.agent_id, msg, level="ok" if approved else "warn", phase="approval")
for sid in SUPERVISOR_IDS:
await terminal_log(sid, f"{msg}", level="ok" if approved else "info", phase="approval")
if add_feed and publish:
feed = add_feed(row.agent_id, msg, "info" if approved else "warn")
await publish({"type": "feed", "entry": feed})
if publish:
await publish({"type": "approval_update", "approval": item})
return item
def list_approvals(db: Session, ApprovalModel: type, status: str = "pending", limit: int = 100) -> list[dict[str, Any]]:
q = select(ApprovalModel).order_by(ApprovalModel.ts.desc()).limit(limit)
if status and status != "all":
q = q.where(ApprovalModel.status == status)
rows = db.execute(q).scalars().all()
return [approval_to_dict(r) for r in rows]
def approval_stats(db: Session, ApprovalModel: type) -> dict[str, int]:
rows = db.execute(select(ApprovalModel)).scalars().all()
stats = {"pending": 0, "approved": 0, "denied": 0, "total": 0}
for r in rows:
stats["total"] += 1
if r.status in stats:
stats[r.status] += 1
return stats
+243
View File
@@ -0,0 +1,243 @@
"""Live database inventory — sizes, row counts, schemas for LLM context."""
from __future__ import annotations
import asyncio
import os
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
from typing import Any
DB_HOST = os.getenv("DB_VAULT_HOST", "10.0.21.51")
PG_USER = os.getenv("PG_USER", "mo")
PG_PASS = os.getenv("PG_PASSWORD", "Dell2026!")
MYSQL_USER = os.getenv("MYSQL_USER", "mo")
MYSQL_PASS = os.getenv("MYSQL_PASSWORD", "Dell2026!")
NEO4J_USER = os.getenv("NEO4J_USER", "neo4j")
NEO4J_PASS = os.getenv("NEO4J_PASSWORD", "testpwd")
ENGINE_TIMEOUT = float(os.getenv("DB_INVENTORY_TIMEOUT", "20"))
_executor = ThreadPoolExecutor(max_workers=4)
def _fmt_bytes(n: int | float | None) -> str:
if n is None:
return "?"
n = float(n)
for unit in ("B", "KB", "MB", "GB", "TB"):
if n < 1024 or unit == "TB":
return f"{n:.1f} {unit}" if unit != "B" else f"{int(n)} B"
n /= 1024
return f"{n:.1f} TB"
def _inventory_postgres() -> dict[str, Any]:
import psycopg2
out: dict[str, Any] = {"engine": "PostgreSQL", "host": DB_HOST, "database": "postgres", "ok": False}
try:
conn = psycopg2.connect(
host=DB_HOST, user=PG_USER, password=PG_PASS, dbname="postgres", connect_timeout=5,
)
cur = conn.cursor()
cur.execute("SELECT pg_database_size(current_database())")
out["size_bytes"] = cur.fetchone()[0]
out["size_human"] = _fmt_bytes(out["size_bytes"])
cur.execute(
"SELECT table_name FROM information_schema.tables "
"WHERE table_schema='public' AND table_type='BASE TABLE' ORDER BY table_name",
)
tables = []
for (tname,) in cur.fetchall():
cur.execute(f'SELECT reltuples::bigint FROM pg_class WHERE relname = %s', (tname,))
est = cur.fetchone()
rows = int(est[0]) if est and est[0] else None
cur.execute(
"SELECT column_name, data_type FROM information_schema.columns "
"WHERE table_schema='public' AND table_name=%s ORDER BY ordinal_position",
(tname,),
)
cols = [f"{c} ({dt})" for c, dt in cur.fetchall()]
tbl: dict[str, Any] = {"name": tname, "rows": rows, "rows_estimated": True, "columns": cols}
if tname == "sales_orders" and rows:
cur.execute(
"SELECT region, COUNT(*) FROM sales_orders TABLESAMPLE SYSTEM (0.1) "
"GROUP BY region ORDER BY COUNT(*) DESC LIMIT 5",
)
sample = cur.fetchall()
if sample:
tbl["sample_regions"] = {r: c for r, c in sample}
tables.append(tbl)
out["tables"] = tables
out["ok"] = True
conn.close()
except Exception as exc:
out["error"] = str(exc)
return out
def _inventory_mysql() -> dict[str, Any]:
import pymysql
out: dict[str, Any] = {"engine": "MySQL", "host": DB_HOST, "database": "hr", "ok": False}
try:
conn = pymysql.connect(
host=DB_HOST, user=MYSQL_USER, password=MYSQL_PASS, database="hr", connect_timeout=5,
)
cur = conn.cursor()
cur.execute(
"SELECT table_name, data_length+index_length, table_rows "
"FROM information_schema.tables WHERE table_schema='hr'",
)
tables = []
total_bytes = 0
for tname, tbytes, trows in cur.fetchall():
total_bytes += tbytes or 0
cur.execute(f"SHOW COLUMNS FROM `{tname}`")
cols = [f"{r[0]} ({r[1]})" for r in cur.fetchall()]
tbl: dict[str, Any] = {
"name": tname,
"rows": int(trows) if trows else None,
"rows_estimated": True,
"size_bytes": tbytes,
"columns": cols,
}
if tname == "employee_events":
tbl["note"] = "HR employee lifecycle events (promotions, transfers, salary changes, etc.)"
tables.append(tbl)
out["tables"] = tables
out["size_bytes"] = total_bytes
out["size_human"] = _fmt_bytes(total_bytes)
out["ok"] = True
conn.close()
except Exception as exc:
out["error"] = str(exc)
return out
def _inventory_mongo() -> dict[str, Any]:
from pymongo import MongoClient
out: dict[str, Any] = {"engine": "MongoDB", "host": DB_HOST, "ok": False}
try:
client = MongoClient(f"mongodb://{DB_HOST}:27017/", serverSelectionTimeoutMS=5000)
db = client["supplychain"]
collections = []
for cname in db.list_collection_names():
if cname.startswith("__"):
continue
col = db[cname]
docs = col.estimated_document_count()
sample = col.find_one() or {}
fields = sorted(k for k in sample if k != "_id")
coll: dict[str, Any] = {"name": cname, "documents": docs, "fields": fields}
if cname == "events" and docs:
try:
pipe = [
{"$sample": {"size": 5000}},
{"$group": {"_id": "$type", "count": {"$sum": 1}}},
{"$sort": {"count": -1}},
{"$limit": 5},
]
coll["sample_types"] = {r["_id"]: r["count"] for r in col.aggregate(pipe, maxTimeMS=5000)}
except Exception:
pass
collections.append(coll)
out["database"] = "supplychain"
out["collections"] = collections
out["ok"] = True
client.close()
except Exception as exc:
out["error"] = str(exc)
return out
def _inventory_cassandra() -> dict[str, Any]:
out: dict[str, Any] = {"engine": "Cassandra", "host": DB_HOST, "ok": False}
try:
from cassandra.cluster import Cluster
cluster = Cluster([DB_HOST], connect_timeout=5)
session = cluster.connect()
keyspaces = [
r.keyspace_name
for r in session.execute("SELECT keyspace_name FROM system_schema.keyspaces")
if r.keyspace_name not in (
"system", "system_schema", "system_traces", "system_distributed",
"system_virtual_schema", "system_auth", "system_views",
)
]
tables_out = []
for ks in keyspaces:
for row in session.execute(
"SELECT table_name FROM system_schema.tables WHERE keyspace_name=%s", (ks,),
):
tables_out.append({
"keyspace": ks,
"name": row.table_name,
"rows": None,
"note": "COUNT skipped (large table; use Trino/Iceberg for analytics)",
})
out["keyspaces"] = keyspaces
out["tables"] = tables_out
out["ok"] = True
cluster.shutdown()
except Exception as exc:
out["error"] = str(exc)
return out
def _inventory_neo4j() -> dict[str, Any]:
out: dict[str, Any] = {"engine": "Neo4j", "host": DB_HOST, "ok": False}
try:
from neo4j import GraphDatabase
driver = GraphDatabase.driver(f"bolt://{DB_HOST}:7687", auth=(NEO4J_USER, NEO4J_PASS))
with driver.session() as session:
nodes = [
{"label": r["lbl"], "count": r["c"]}
for r in session.run(
"MATCH (n) RETURN labels(n)[0] AS lbl, count(*) AS c ORDER BY c DESC LIMIT 10",
)
]
rels = [
{"type": r["t"], "count": r["c"]}
for r in session.run(
"MATCH ()-[r]->() RETURN type(r) AS t, count(*) AS c ORDER BY c DESC LIMIT 10",
)
]
out["nodes"] = nodes
out["relationships"] = rels
out["ok"] = True
driver.close()
except Exception as exc:
out["error"] = str(exc)
return out
def _run_with_timeout(fn, timeout: float) -> dict[str, Any]:
future = _executor.submit(fn)
try:
return future.result(timeout=timeout)
except FuturesTimeout:
return {"engine": fn.__name__.replace("_inventory_", ""), "ok": False, "error": f"timeout after {timeout}s"}
except Exception as exc:
return {"ok": False, "error": str(exc)}
def collect_database_inventory_sync() -> dict[str, Any]:
fns = {
"postgresql": _inventory_postgres,
"mysql": _inventory_mysql,
"mongodb": _inventory_mongo,
"cassandra": _inventory_cassandra,
"neo4j": _inventory_neo4j,
}
engines = {k: _run_with_timeout(fn, ENGINE_TIMEOUT) for k, fn in fns.items()}
ok_count = sum(1 for e in engines.values() if e.get("ok"))
return {"host": DB_HOST, "engines_ok": ok_count, "engines_total": len(engines), "engines": engines}
async def collect_database_inventory() -> dict[str, Any]:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(_executor, collect_database_inventory_sync)
+164
View File
@@ -0,0 +1,164 @@
"""Database engine, session factory, and startup migrations."""
from __future__ import annotations
import os
import sqlite3
from typing import Any
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.orm import sessionmaker
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:////data/atc-agents.db")
SQLITE_FALLBACK_PATH = os.getenv("SQLITE_FALLBACK_PATH", "/data/atc-agents.db")
def make_engine(url: str = DATABASE_URL):
kwargs: dict[str, Any] = {"pool_pre_ping": True}
if url.startswith("sqlite"):
kwargs["connect_args"] = {"check_same_thread": False}
return create_engine(url, **kwargs)
engine = make_engine()
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def migrate_approval_columns(db_engine: Any = engine) -> None:
"""Add approval columns on legacy SQLite/Postgres schemas."""
insp = inspect(db_engine)
if "approvals" not in insp.get_table_names():
return
existing = {c["name"] for c in insp.get_columns("approvals")}
dialect = db_engine.dialect.name
if dialect == "postgresql":
alters = [
("action_type", "VARCHAR(64)", "generic.mutate"),
("target", "TEXT", ""),
("payload", "TEXT", "{}"),
("decided_by", "VARCHAR(64)", None),
("decide_note", "TEXT", None),
("decided_at", "TIMESTAMP WITH TIME ZONE", None),
("priority", "VARCHAR(16)", "normal"),
]
with db_engine.begin() as conn:
for col, typ, default in alters:
if col in existing:
continue
if default is None:
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ}"))
elif default == "":
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ} DEFAULT ''"))
else:
conn.execute(
text(f"ALTER TABLE approvals ADD COLUMN IF NOT EXISTS {col} {typ} DEFAULT '{default}'")
)
return
alters = [
("action_type", "VARCHAR(64)", "'generic.mutate'"),
("target", "TEXT", "''"),
("payload", "TEXT", "'{}'"),
("decided_by", "VARCHAR(64)", "NULL"),
("decide_note", "TEXT", "NULL"),
("decided_at", "DATETIME", "NULL"),
("priority", "VARCHAR(16)", "'normal'"),
]
with db_engine.begin() as conn:
for col, typ, default in alters:
if col not in existing:
conn.execute(text(f"ALTER TABLE approvals ADD COLUMN {col} {typ} DEFAULT {default}"))
def migrate_sqlite_to_postgres(db_engine: Any = engine) -> dict[str, int]:
"""One-time copy from legacy SQLite volume into Postgres when Postgres is empty."""
if not DATABASE_URL.startswith("postgresql"):
return {}
if not os.path.isfile(SQLITE_FALLBACK_PATH):
return {}
insp = inspect(db_engine)
tables = set(insp.get_table_names())
if "approvals" not in tables or "feed" not in tables:
return {}
with db_engine.connect() as conn:
approval_count = conn.execute(text("SELECT COUNT(*) FROM approvals")).scalar() or 0
feed_count = conn.execute(text("SELECT COUNT(*) FROM feed")).scalar() or 0
if approval_count or feed_count:
return {"approvals": 0, "feed": 0, "skipped": 1}
copied = {"approvals": 0, "feed": 0}
src = sqlite3.connect(SQLITE_FALLBACK_PATH)
src.row_factory = sqlite3.Row
try:
with db_engine.begin() as conn:
for row in src.execute("SELECT * FROM feed"):
conn.execute(
text(
"INSERT INTO feed (id, ts, agent_id, level, message) "
"VALUES (:id, :ts, :agent_id, :level, :message) ON CONFLICT (id) DO NOTHING"
),
dict(row),
)
copied["feed"] += 1
for row in src.execute("SELECT * FROM approvals"):
conn.execute(
text(
"INSERT INTO approvals (id, ts, agent_id, action, reason, status, "
"action_type, target, payload, decided_by, decide_note, decided_at, priority) "
"VALUES (:id, :ts, :agent_id, :action, :reason, :status, "
":action_type, :target, :payload, :decided_by, :decide_note, :decided_at, :priority) "
"ON CONFLICT (id) DO NOTHING"
),
{
"id": row["id"],
"ts": row["ts"],
"agent_id": row["agent_id"],
"action": row["action"],
"reason": row["reason"],
"status": row["status"],
"action_type": row["action_type"] if "action_type" in row.keys() else "generic.mutate",
"target": row["target"] if "target" in row.keys() else "",
"payload": row["payload"] if "payload" in row.keys() else "{}",
"decided_by": row["decided_by"] if "decided_by" in row.keys() else None,
"decide_note": row["decide_note"] if "decide_note" in row.keys() else None,
"decided_at": row["decided_at"] if "decided_at" in row.keys() else None,
"priority": row["priority"] if "priority" in row.keys() else "normal",
},
)
copied["approvals"] += 1
finally:
src.close()
return copied
def init_database(Base: type) -> dict[str, Any]:
"""Create tables, run migrations, optionally import legacy SQLite data."""
Base.metadata.create_all(engine)
migrate_approval_columns(engine)
migrated = migrate_sqlite_to_postgres(engine)
return {"engine": engine.dialect.name, "migrated_from_sqlite": migrated}
def db_health() -> dict[str, Any]:
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
insp = inspect(engine)
tables = insp.get_table_names()
stats = {}
if "approvals" in tables:
with engine.connect() as conn:
stats["approvals"] = conn.execute(text("SELECT COUNT(*) FROM approvals")).scalar()
stats["approvals_pending"] = conn.execute(
text("SELECT COUNT(*) FROM approvals WHERE status = 'pending'")
).scalar()
if "feed" in tables:
with engine.connect() as conn:
stats["feed"] = conn.execute(text("SELECT COUNT(*) FROM feed")).scalar()
return {"ok": True, "dialect": engine.dialect.name, "tables": tables, **stats}
except Exception as exc:
return {"ok": False, "dialect": engine.dialect.name, "error": str(exc)}
+20
View File
@@ -0,0 +1,20 @@
"""Dockhand environment IDs on atc-docker01 (10.0.21.45:8082)."""
DOCKHAND_URL = "http://10.0.21.45:8082"
DOCKHAND_ENVS: dict[str, int] = {
"docker01": 1,
"docker02": 2,
"management": 3,
"db02": 5,
"bart_gpu": 6,
"mo_gpu": 7,
"gpu_dev": 8,
"lakehouse": 9,
"airflow": 10,
"dns1": 11,
"dns2": 12,
"command_center": 13,
}
DOCKHAND_ENV_COMMAND_CENTER = 13
+745
View File
@@ -0,0 +1,745 @@
"""Live lab metrics for all ATC domains — fed to vLLM as context."""
from __future__ import annotations
import asyncio
import json
import os
import time
from datetime import datetime, timezone
from typing import Any
import httpx
from agent_terminal import TerminalLogFn
from dockhand_envs import DOCKHAND_ENV_COMMAND_CENTER, DOCKHAND_ENVS
from database_inventory import collect_database_inventory
from node_registry import NODE_REGISTRY
DOCKHAND_URL = os.getenv("DOCKHAND_URL", "http://10.0.21.45:8082")
HDFS_NN_URL = os.getenv("HDFS_NN_URL", "http://10.0.21.61:9870")
LAKEHOUSE_HOST = os.getenv("LAKEHOUSE_HOST", "10.0.21.50")
AIRFLOW_URL = os.getenv("AIRFLOW_URL", "http://10.0.21.55:8080")
KAFKA_UI_URL = os.getenv("KAFKA_UI_URL", "http://10.0.21.36:9000")
KAFKA_CONNECT_URL = os.getenv("KAFKA_CONNECT_URL", f"http://{LAKEHOUSE_HOST}:8083")
TRINO_URL = os.getenv("TRINO_URL", f"http://{LAKEHOUSE_HOST}:8089")
SPARK_UI_URL = os.getenv("SPARK_UI_URL", f"http://{LAKEHOUSE_HOST}:8080")
GPU_URL = os.getenv("GPU_URL", "http://10.0.20.106:9000")
OBJECTSCALE_URL = os.getenv("OBJECTSCALE_URL", "http://10.0.20.111:9020")
YARN_URL = os.getenv("YARN_URL", "http://10.0.21.61:8088")
AGENT_PRIMARY_DOMAIN = {
"infra-sentinel": "docker",
"data-custodian": "databases",
"lakehouse-ops": "lakehouse",
"hadoop-ranger": "hadoop",
"etl-guardian": "etl",
}
async def _log(log: TerminalLogFn | None, level: str, phase: str, text: str) -> None:
if log:
await log(level, phase, text)
async def _get_json(
client: httpx.AsyncClient,
url: str,
log: TerminalLogFn | None = None,
label: str = "",
timeout: float = 6.0,
) -> Any | None:
name = label or url
t0 = time.monotonic()
await _log(log, "cmd", "fetch", f"$ GET {url}")
try:
r = await client.get(url, timeout=timeout)
ms = int((time.monotonic() - t0) * 1000)
if r.status_code < 400:
await _log(log, "ok", "fetch", f"{r.status_code} {name} ({ms}ms)")
return r.json()
await _log(log, "warn", "fetch", f"{r.status_code} {name} ({ms}ms)")
except Exception as exc:
ms = int((time.monotonic() - t0) * 1000)
await _log(log, "err", "fetch", f"{name}: {exc} ({ms}ms)")
return None
async def _probe_ok(
client: httpx.AsyncClient,
url: str,
log: TerminalLogFn | None = None,
label: str = "",
) -> bool:
name = label or url
t0 = time.monotonic()
await _log(log, "cmd", "probe", f"$ GET {url}")
try:
r = await client.get(url, timeout=4.0)
ms = int((time.monotonic() - t0) * 1000)
ok = r.status_code < 500
await _log(log, "ok" if ok else "warn", "probe", f"{r.status_code} {name} ({'UP' if ok else 'DOWN'}, {ms}ms)")
return ok
except Exception as exc:
ms = int((time.monotonic() - t0) * 1000)
await _log(log, "err", "probe", f"{name}: {exc} ({ms}ms)")
return False
def _container_rows(containers: list[dict], host: str = "") -> list[dict[str, Any]]:
rows = []
for c in containers:
ports = sorted({str(p.get("PublicPort")) for p in c.get("ports", []) if p.get("PublicPort")})
rows.append({
"name": c.get("name"),
"state": c.get("state"),
"image": c.get("image"),
"status": c.get("status"),
"ports": ports,
"host": host,
})
return rows
async def dockhand_containers(
client: httpx.AsyncClient,
env_id: int,
log: TerminalLogFn | None = None,
) -> list[dict]:
url = f"{DOCKHAND_URL}/api/containers?env={env_id}"
await _log(log, "cmd", "fetch", f"$ GET {url}")
t0 = time.monotonic()
try:
r = await client.get(f"{DOCKHAND_URL}/api/containers", params={"env": env_id}, timeout=8.0)
ms = int((time.monotonic() - t0) * 1000)
r.raise_for_status()
data = r.json()
await _log(log, "ok", "fetch", f"← Dockhand env {env_id}: {len(data)} containers ({ms}ms)")
return data
except Exception as exc:
ms = int((time.monotonic() - t0) * 1000)
await _log(log, "err", "fetch", f"✗ Dockhand env {env_id}: {exc} ({ms}ms)")
return []
async def collect_hdfs(client: httpx.AsyncClient, log: TerminalLogFn | None = None) -> dict[str, Any]:
ctx: dict[str, Any] = {"reachable": False, "namenode": HDFS_NN_URL}
await _log(log, "info", "fetch", "▸ HDFS NameNode JMX metrics")
try:
fs_url = f"{HDFS_NN_URL}/jmx?qry=Hadoop:service=NameNode,name=FSNamesystem"
nn_url = f"{HDFS_NN_URL}/jmx?qry=Hadoop:service=NameNode,name=NameNodeInfo"
t0 = time.monotonic()
await _log(log, "cmd", "fetch", f"$ GET {fs_url}")
await _log(log, "cmd", "fetch", f"$ GET {nn_url}")
fs_r, nn_r = await asyncio.gather(
client.get(fs_url),
client.get(nn_url),
return_exceptions=True,
)
ms = int((time.monotonic() - t0) * 1000)
if isinstance(fs_r, httpx.Response) and fs_r.status_code == 200:
beans = fs_r.json().get("beans", [])
if beans:
b = beans[0]
ctx.update({
"reachable": True,
"hostname": b.get("tag.Hostname"),
"ha_state": b.get("tag.HAState"),
"capacity_total_gb": b.get("CapacityTotalGB"),
"capacity_used_gb": b.get("CapacityUsedGB"),
"capacity_remaining_gb": b.get("CapacityRemainingGB"),
"files_total": b.get("FilesTotal"),
"blocks_total": b.get("BlocksTotal"),
"live_datanodes": b.get("NumLiveDataNodes"),
"dead_datanodes": b.get("NumDeadDataNodes"),
"missing_blocks": b.get("MissingBlocks"),
"under_replicated_blocks": b.get("UnderReplicatedBlocks"),
"corrupt_blocks": b.get("CorruptBlocks"),
"default_replication_factor": 3,
})
await _log(
log, "ok", "fetch",
f"← HDFS: {b.get('CapacityUsedGB')}GB used, {b.get('FilesTotal')} files, "
f"{b.get('NumLiveDataNodes')} datanodes ({ms}ms)",
)
else:
await _log(log, "warn", "fetch", f"← FSNamesystem JMX failed ({ms}ms)")
if isinstance(nn_r, httpx.Response) and nn_r.status_code == 200:
beans = nn_r.json().get("beans", [])
if beans:
b = beans[0]
live = json.loads(b.get("LiveNodes") or "{}")
ctx["hdfs_version"] = b.get("Version")
ctx["safemode"] = b.get("Safemode") or "off"
ctx["percent_used"] = round(float(b.get("PercentUsed", 0)) * 100, 4)
ctx["datanodes"] = [
{
"host": host.split(":")[0],
"capacity_gb": round(node.get("capacity", 0) / (1024**3), 1),
"used_gb": round(node.get("used", 0) / (1024**3), 4),
"blocks": node.get("numBlocks", 0),
"state": node.get("adminState"),
}
for host, node in live.items()
]
yarn_url = f"{YARN_URL}/ws/v1/cluster/info"
await _log(log, "cmd", "fetch", f"$ GET {yarn_url}")
try:
yr = await client.get(yarn_url, timeout=4.0)
if yr.status_code == 200:
yinfo = yr.json().get("clusterInfo", {})
ctx["yarn_ok"] = True
ctx["yarn_state"] = yinfo.get("state", "UNKNOWN")
ctx["yarn_rm"] = YARN_URL
await _log(log, "ok", "fetch", f"← YARN RM: {yinfo.get('state', '?')}")
else:
ctx["yarn_ok"] = False
except Exception:
ctx["yarn_ok"] = False
except Exception as exc:
ctx["error"] = str(exc)
await _log(log, "err", "fetch", f"✗ HDFS: {exc}")
return ctx
async def collect_etl(client: httpx.AsyncClient, log: TerminalLogFn | None = None) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ ETL stack (Airflow, Kafka, Spark)")
health, kafka_ok, spark_ok = await asyncio.gather(
_get_json(client, f"{AIRFLOW_URL}/api/v2/monitor/health", log, "Airflow health"),
_probe_ok(client, KAFKA_UI_URL, log, "Kafka UI"),
_probe_ok(client, SPARK_UI_URL, log, "Spark UI"),
)
connectors: list[str] = []
await _log(log, "cmd", "fetch", f"$ GET {KAFKA_CONNECT_URL}/connectors")
t0 = time.monotonic()
try:
r = await client.get(f"{KAFKA_CONNECT_URL}/connectors", timeout=5.0)
ms = int((time.monotonic() - t0) * 1000)
if r.status_code == 200:
connectors = r.json() if isinstance(r.json(), list) else []
await _log(log, "ok", "fetch", f"← Kafka Connect: {len(connectors)} connectors ({ms}ms)")
for c in connectors:
await _log(log, "info", "fetch", f" · {c}")
else:
await _log(log, "warn", "fetch", f"← Kafka Connect {r.status_code} ({ms}ms)")
except Exception as exc:
await _log(log, "err", "fetch", f"✗ Kafka Connect: {exc}")
airflow_detail: dict[str, str] = {}
if isinstance(health, dict):
for comp, info in health.items():
if isinstance(info, dict) and "status" in info:
airflow_detail[comp] = info["status"]
await _log(log, "info", "fetch", f" Airflow {comp}: {info['status']}")
connector_status: list[dict[str, Any]] = []
for name in connectors:
status_url = f"{KAFKA_CONNECT_URL}/connectors/{name}/status"
await _log(log, "cmd", "fetch", f"$ GET {status_url}")
try:
sr = await client.get(status_url, timeout=5.0)
if sr.status_code == 200:
st = sr.json()
conn = st.get("connector", {})
tasks = st.get("tasks", [])
state = conn.get("state", "UNKNOWN")
task_states = [t.get("state", "?") for t in tasks]
connector_status.append({
"name": name,
"state": state,
"tasks": task_states,
})
await _log(log, "info", "fetch", f" · {name}: {state} tasks={task_states}")
except Exception as exc:
connector_status.append({"name": name, "state": "ERROR", "error": str(exc)})
return {
"airflow_url": AIRFLOW_URL,
"airflow_healthy": airflow_detail.get("scheduler") == "healthy",
"airflow_components": airflow_detail,
"kafka_ui_url": KAFKA_UI_URL,
"kafka_ui_ok": kafka_ok,
"kafka_connect_url": KAFKA_CONNECT_URL,
"connectors": connectors,
"connector_status": connector_status,
"spark_ui_url": SPARK_UI_URL,
"spark_ui_ok": spark_ok,
}
async def collect_lakehouse(
client: httpx.AsyncClient,
containers: list[dict],
log: TerminalLogFn | None = None,
) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ Lakehouse (Trino, Spark, Kafka Connect)")
trino_info = await _get_json(client, f"{TRINO_URL}/v1/info", log, "Trino /v1/info")
running = sum(1 for c in containers if c.get("state") == "running")
for c in containers:
ports = ",".join(str(p.get("PublicPort")) for p in c.get("ports", []) if p.get("PublicPort")) or "internal"
await _log(log, "info", "fetch", f" · {c.get('name')}: {c.get('state')} ports={ports}")
return {
"host": LAKEHOUSE_HOST,
"trino_url": TRINO_URL,
"trino_ok": trino_info is not None,
"trino_version": (trino_info or {}).get("nodeVersion", {}).get("version"),
"trino_uptime": (trino_info or {}).get("uptime"),
"trino_coordinator": (trino_info or {}).get("coordinator"),
"spark_ui_url": SPARK_UI_URL,
"kafka_connect_url": KAFKA_CONNECT_URL,
"containers": _container_rows(containers, LAKEHOUSE_HOST),
"running": running,
"total": len(containers),
}
async def collect_databases(
client: httpx.AsyncClient,
containers: list[dict],
log: TerminalLogFn | None = None,
) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ Database vault (Dockhand env 5)")
running = sum(1 for c in containers if c.get("state") == "running")
rows = _container_rows(containers)
by_engine: dict[str, list[str]] = {}
for r in rows:
img = (r.get("image") or "").lower()
name = (r.get("name") or "").lower()
if "postgres" in img or "postgres" in name:
engine = "PostgreSQL"
elif "mysql" in img or "mysql" in name:
engine = "MySQL"
elif "mongo" in img or "mongo" in name:
engine = "MongoDB"
elif "cassandra" in img or "cassandra" in name:
engine = "Cassandra"
elif "neo4j" in img or "neo4j" in name:
engine = "Neo4j"
else:
engine = "Other"
port_str = ",".join(r["ports"]) or "internal"
by_engine.setdefault(engine, []).append(f"{r['name']} ({r['state']}, ports {port_str})")
await _log(log, "info", "fetch", f" · {r['name']}: {r['state']} [{engine}] ports={port_str}")
return {
"dockhand_env": 5,
"running": running,
"total": len(containers),
"containers": rows,
"by_engine": by_engine,
}
async def collect_command_center(
client: httpx.AsyncClient,
containers: list[dict],
log: TerminalLogFn | None = None,
) -> dict[str, Any]:
await _log(log, "info", "fetch", f"▸ Command Center VM304 (Dockhand env {DOCKHAND_ENV_COMMAND_CENTER})")
running = sum(1 for c in containers if c.get("state") == "running")
rows = _container_rows(containers, "10.0.21.33")
for r in rows:
lvl = "info" if r.get("state") == "running" else "warn"
await _log(log, lvl, "fetch", f" · {r.get('name')}: {r.get('state')}")
return {
"dockhand_env": DOCKHAND_ENV_COMMAND_CENTER,
"dockhand_stack": "atc-agents-vm304",
"host": "10.0.21.33",
"vmid": 304,
"url": "http://10.0.21.33/",
"running": running,
"total": len(containers),
"containers": rows,
}
async def collect_docker_rack(
client: httpx.AsyncClient,
containers: list[dict],
log: TerminalLogFn | None = None,
) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ Docker rack (Dockhand env 1)")
running = sum(1 for c in containers if c.get("state") == "running")
not_running = [c["name"] for c in containers if c.get("state") != "running"]
for c in containers:
ports = ",".join(str(p.get("PublicPort")) for p in c.get("ports", []) if p.get("PublicPort")) or "internal"
lvl = "info" if c.get("state") == "running" else "warn"
await _log(log, lvl, "fetch", f" · {c.get('name')}: {c.get('state')} ports={ports}")
return {
"dockhand_url": DOCKHAND_URL,
"dockhand_env": 1,
"running": running,
"total": len(containers),
"not_running": not_running,
"containers": _container_rows(containers, "10.0.21.45"),
}
async def collect_objectscale(client: httpx.AsyncClient, log: TerminalLogFn | None = None) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ ObjectScale S3 storage")
ctx: dict[str, Any] = {
"host": "10.0.20.111",
"url": OBJECTSCALE_URL,
"port": "9020",
"bucket": "data",
"reachable": False,
}
t0 = time.monotonic()
await _log(log, "cmd", "probe", f"$ GET {OBJECTSCALE_URL}")
try:
r = await client.get(OBJECTSCALE_URL, timeout=4.0)
ms = int((time.monotonic() - t0) * 1000)
# 403/401 means API is up but unauthenticated
ctx["reachable"] = r.status_code in (200, 401, 403, 405)
ctx["status_code"] = r.status_code
await _log(
log, "ok" if ctx["reachable"] else "warn", "probe",
f"← ObjectScale {r.status_code} ({'UP' if ctx['reachable'] else 'DOWN'}, {ms}ms)",
)
except Exception as exc:
ms = int((time.monotonic() - t0) * 1000)
ctx["error"] = str(exc)
await _log(log, "err", "probe", f"✗ ObjectScale: {exc} ({ms}ms)")
return ctx
async def collect_gpu_metrics(client: httpx.AsyncClient, log: TerminalLogFn | None = None) -> dict[str, Any]:
await _log(log, "info", "fetch", "▸ GPU Lab metrics")
base = {"ok": False, "host": GPU_URL, "ui_url": GPU_URL}
try:
metrics_url = f"{GPU_URL}/api/gpu/metrics"
model_url = f"{GPU_URL}/api/active-model"
await _log(log, "cmd", "fetch", f"$ GET {metrics_url}")
await _log(log, "cmd", "fetch", f"$ GET {model_url}")
t0 = time.monotonic()
metrics_r, model_r = await asyncio.gather(
client.get(metrics_url),
client.get(model_url),
return_exceptions=True,
)
ms = int((time.monotonic() - t0) * 1000)
gpus: list[dict[str, Any]] = []
if isinstance(metrics_r, httpx.Response) and metrics_r.status_code == 200:
current = metrics_r.json().get("current", {})
gpus = [
{
"index": g["index"],
"name": g["name"],
"util_gpu": g.get("util_gpu", 0),
"memory_used_mib": g.get("memory_used_mib", 0),
"memory_total_mib": g.get("memory_total_mib", 0),
"temperature_c": g.get("temperature_c", 0),
"power_w": g.get("power_w", 0),
}
for g in current.get("gpus", [])
]
await _log(log, "ok", "fetch", f"← GPU metrics: {len(gpus)} devices ({ms}ms)")
for g in gpus:
await _log(
log, "info", "fetch",
f" GPU{g['index']}: util {g['util_gpu']:.0f}% VRAM "
f"{g['memory_used_mib']:.0f}/{g['memory_total_mib']:.0f} MiB",
)
active_model = None
inference_active = False
vllm_url = None
if isinstance(model_r, httpx.Response) and model_r.status_code == 200:
model_data = model_r.json()
active_model = model_data.get("name")
inference_active = bool(model_data.get("inference_active"))
vllm_url = model_data.get("base_url")
await _log(log, "ok", "fetch", f"← Active model: {active_model} inference={'ON' if inference_active else 'OFF'}")
return {
**base,
"ok": len(gpus) > 0 or inference_active,
"inference_active": inference_active,
"active_model": active_model,
"vllm_url": vllm_url,
"gpu_count": len(gpus),
"gpus": gpus,
}
except Exception as exc:
await _log(log, "err", "fetch", f"✗ GPU Lab: {exc}")
return {**base, "error": str(exc)}
def _section_docker(d: dict[str, Any]) -> list[str]:
lines = [
f"Docker rack (Dockhand env 1): {d['running']}/{d['total']} running",
f"Dockhand: {d['dockhand_url']}",
]
if d.get("not_running"):
lines.append(f"Not running: {', '.join(d['not_running'])}")
for c in d.get("containers", []):
port_str = ",".join(c["ports"]) if c["ports"] else "internal"
lines.append(f" - {c['name']}: {c['state']} | {c['image']} | ports {port_str}")
return lines
def _fmt_count(n: Any) -> str:
if n is None:
return "?"
try:
return f"{int(n):,}"
except (TypeError, ValueError):
return str(n)
def _section_databases(d: dict[str, Any]) -> list[str]:
lines = [f"Databases (Dockhand env {d['dockhand_env']}): {d['running']}/{d['total']} running"]
for engine, items in d.get("by_engine", {}).items():
lines.append(f" {engine}:")
for item in items:
lines.append(f" - {item}")
inv = d.get("inventory") or {}
if inv:
lines.append(
f" Live data inventory @ {inv.get('host', '?')}: "
f"{inv.get('engines_ok', 0)}/{inv.get('engines_total', 0)} engines queried"
)
for key, eng in (inv.get("engines") or {}).items():
if not eng.get("ok"):
err = str(eng.get("error", "unknown"))[:100]
lines.append(f" {eng.get('engine', key)}: ERROR — {err}")
continue
label = eng.get("engine", key)
if eng.get("size_human"):
lines.append(f" {label} ({eng.get('database', '')}): {eng['size_human']}")
for tbl in eng.get("tables") or []:
rows = tbl.get("rows")
cols = ", ".join((tbl.get("columns") or [])[:8])
extra = ""
if tbl.get("top_regions"):
extra = f" | regions: {tbl['top_regions']}"
elif tbl.get("top_event_types"):
extra = f" | event_types: {tbl['top_event_types']}"
lines.append(f" · {tbl['name']}: {_fmt_count(rows)} rows | cols: {cols}{extra}")
for coll in eng.get("collections") or []:
docs = coll.get("documents")
fields = ", ".join(coll.get("fields") or [])
extra = f" | types: {coll['top_types']}" if coll.get("top_types") else ""
lines.append(f" · {coll['name']}: {_fmt_count(docs)} docs | fields: {fields}{extra}")
for node in eng.get("nodes") or []:
lines.append(f" · {node['label']} nodes: {_fmt_count(node.get('count'))}")
if eng.get("relationships"):
rels = ", ".join(f"{r['type']}={_fmt_count(r.get('count'))}" for r in eng["relationships"][:5])
lines.append(f" · relationships: {rels or 'none'}")
return lines
def _section_lakehouse(d: dict[str, Any]) -> list[str]:
lines = [
f"Lakehouse host: {d['host']}{d['running']}/{d['total']} containers running",
f"Trino: {d['trino_url']}{'UP' if d['trino_ok'] else 'DOWN'}"
+ (f" (v{d['trino_version']}, uptime {d.get('trino_uptime')})" if d.get("trino_ok") else ""),
f"Spark UI: {d['spark_ui_url']}",
f"Kafka Connect: {d['kafka_connect_url']}",
]
for c in d.get("containers", []):
port_str = ",".join(c["ports"]) if c["ports"] else "internal"
lines.append(f" - {c['name']}: {c['state']} | {c['image']} | ports {port_str}")
return lines
def _section_etl(d: dict[str, Any]) -> list[str]:
lines = [
f"Airflow ({d['airflow_url']}): {'HEALTHY' if d['airflow_healthy'] else 'DEGRADED'}",
]
for comp, st in d.get("airflow_components", {}).items():
lines.append(f" - {comp}: {st}")
lines.append(f"Kafka UI ({d['kafka_ui_url']}): {'UP' if d['kafka_ui_ok'] else 'DOWN'}")
lines.append(f"Kafka Connect ({d['kafka_connect_url']}): connectors {d.get('connectors') or 'none listed'}")
if d.get("connectors"):
lines.append(" Registered connector names (exact): " + ", ".join(d["connectors"]))
for cs in d.get("connector_status") or []:
tasks = cs.get("tasks") or []
lines.append(f" Connector {cs['name']}: {cs.get('state', '?')}" + (f" tasks={tasks}" if tasks else ""))
lines.append(f"Spark UI ({d['spark_ui_url']}): {'UP' if d['spark_ui_ok'] else 'DOWN'}")
return lines
def _section_hadoop(h: dict[str, Any]) -> list[str]:
lines = ["HDFS / Hadoop:"]
if not h.get("reachable"):
lines.append(f" UNREACHABLE: {h.get('error', 'NameNode probe failed')}")
return lines
lines.extend([
f" NameNode: {h['namenode']} ({h.get('hostname')}, HA {h.get('ha_state')})",
f" Version: {h.get('hdfs_version')}, safemode: {h.get('safemode')}",
f" Capacity: {h.get('capacity_used_gb')} GB used / {h.get('capacity_total_gb')} GB total "
f"({h.get('capacity_remaining_gb')} GB free, {h.get('percent_used', 0)}% used)",
f" Files: {h.get('files_total')}, Blocks: {h.get('blocks_total')}",
f" DataNodes: {h.get('live_datanodes')} live, {h.get('dead_datanodes')} dead",
f" Replication factor (dfs.replication): {h.get('default_replication_factor')}",
f" Block health: missing={h.get('missing_blocks')}, under-replicated={h.get('under_replicated_blocks')}, corrupt={h.get('corrupt_blocks')}",
])
for dn in h.get("datanodes", []):
lines.append(
f" - {dn['host']}: {dn['used_gb']} GB / {dn['capacity_gb']} GB, {dn['blocks']} blocks, {dn['state']}"
)
if (h.get("capacity_used_gb") or 0) < 0.01 and (h.get("files_total") or 0) > 0:
lines.append(" Note: metadata/small files only — almost no user data stored yet.")
return lines
def _section_gpu(g: dict[str, Any]) -> list[str]:
lines = ["GPU Lab / vLLM inference:"]
if not g.get("ok"):
lines.append(f" OFFLINE: {g.get('error', 'unreachable')}")
return lines
lines.extend([
f" Manager: {g.get('ui_url')}",
f" Model: {g.get('active_model')} (inference {'ON' if g.get('inference_active') else 'OFF'})",
f" vLLM endpoint: {g.get('vllm_url')}",
f" GPUs: {g.get('gpu_count')}x V100",
])
for gpu in g.get("gpus", []):
lines.append(
f" GPU{gpu['index']}: util {gpu['util_gpu']:.0f}%, "
f"VRAM {gpu['memory_used_mib']:.0f}/{gpu['memory_total_mib']:.0f} MiB, "
f"{gpu['temperature_c']}°C, {gpu['power_w']:.0f}W"
)
return lines
def _section_objectscale(o: dict[str, Any]) -> list[str]:
lines = [
f"ObjectScale S3 ({o.get('host')}:{o.get('port')}): {'REACHABLE' if o.get('reachable') else 'DOWN'}",
f" API: {o.get('url')} (HTTP {o.get('status_code', '?')})",
f" Bucket: {o.get('bucket', 'data')} — landing zone for s3-kafka-consumer & Iceberg",
]
if o.get("error"):
lines.append(f" Error: {o['error']}")
return lines
def _section_command_center(c: dict[str, Any]) -> list[str]:
lines = [
f"Command Center VM304: {c.get('host')}{c.get('running', 0)}/{c.get('total', 0)} containers",
f" URL: {c.get('url')}",
f" Dockhand env: {c.get('dockhand_env')}",
]
for row in c.get("containers", []):
port_str = ",".join(row["ports"]) if row.get("ports") else "internal"
lines.append(f" - {row['name']}: {row['state']} | ports {port_str}")
return lines
def _section_cluster_registry(_: dict[str, Any]) -> list[str]:
"""Static cluster map — always available even when probes fail."""
lines = ["Cluster infrastructure map (Proxmox VMs & roles):"]
for nid, node in NODE_REGISTRY.items():
if nid in ("mo-commander", "bart-commander", "network-watcher", "mcp-coordinator"):
continue
vmid = node.get("vmid", "?")
lines.append(
f" - {node['label']}: {node.get('vm')} VMID {vmid} @ {node.get('ip')}{node.get('role')}"
)
desc = node.get("description") or ""
if desc:
lines.append(f" {desc[:140]}")
lines.append("")
lines.append("Supervisors & control plane:")
for nid in ("mo-commander", "bart-commander", "mcp-coordinator", "network-watcher"):
node = NODE_REGISTRY[nid]
lines.append(f" - {node['label']}: {(node.get('description') or '')[:120]}")
return lines
SECTION_BUILDERS = {
"docker": _section_docker,
"databases": _section_databases,
"lakehouse": _section_lakehouse,
"etl": _section_etl,
"hadoop": _section_hadoop,
"gpu": _section_gpu,
"objectscale": _section_objectscale,
"command_center": _section_command_center,
"cluster_registry": _section_cluster_registry,
}
DOMAIN_ORDER = ["docker", "databases", "lakehouse", "etl", "hadoop", "gpu", "objectscale", "command_center", "cluster_registry"]
async def collect_full_lab_context(
gpu_data: dict[str, Any] | None = None,
log: TerminalLogFn | None = None,
include_inventory: bool = True,
) -> dict[str, Any]:
"""Gather all lab domains in parallel with optional live terminal logging."""
await _log(log, "info", "fetch", "═══ Lab snapshot collection started ═══")
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
if gpu_data is None:
gpu_data = await collect_gpu_metrics(client, log)
docker_raw, db_raw, lake_raw, cc_raw, hdfs, etl, objectscale = await asyncio.gather(
dockhand_containers(client, DOCKHAND_ENVS["docker01"], log),
dockhand_containers(client, DOCKHAND_ENVS["db02"], log),
dockhand_containers(client, DOCKHAND_ENVS["lakehouse"], log),
dockhand_containers(client, DOCKHAND_ENV_COMMAND_CENTER, log),
collect_hdfs(client, log),
collect_etl(client, log),
collect_objectscale(client, log),
)
docker = await collect_docker_rack(client, docker_raw, log)
databases = await collect_databases(client, db_raw, log)
if include_inventory:
try:
databases["inventory"] = await collect_database_inventory()
inv_ok = databases["inventory"].get("engines_ok", 0)
await _log(log, "ok", "fetch", f"← Database inventory: {inv_ok} engines")
except Exception as exc:
await _log(log, "warn", "fetch", f"✗ Database inventory: {exc}")
databases["inventory"] = {"error": str(exc)}
lakehouse = await collect_lakehouse(client, lake_raw, log)
command_center = await collect_command_center(client, cc_raw, log)
await _log(log, "ok", "fetch", "═══ Lab snapshot complete ═══")
return {
"ts": datetime.now(timezone.utc).isoformat(),
"docker": docker,
"databases": databases,
"lakehouse": lakehouse,
"etl": etl,
"hadoop": hdfs,
"gpu": gpu_data,
"objectscale": objectscale,
"command_center": command_center,
}
def format_context_for_agent(agent_id: str, snapshot: dict[str, Any]) -> str:
"""Format full lab snapshot for LLM; primary domain first."""
primary = AGENT_PRIMARY_DOMAIN.get(agent_id, "docker")
lines = [
f"ATC Lab live snapshot — {snapshot.get('ts')}",
f"Your primary domain: {primary.upper()}",
]
if snapshot.get("domains_summary"):
lines.append(f"Health summary: {json.dumps(snapshot['domains_summary'], default=str)}")
lines.extend(["", f"=== PRIMARY: {primary.upper()} ==="])
if primary in snapshot and primary in SECTION_BUILDERS:
lines.extend(SECTION_BUILDERS[primary](snapshot[primary]))
lines.append("")
lines.append("=== FULL LAB (all domains) ===")
if "cluster_registry" not in snapshot:
snapshot = {**snapshot, "cluster_registry": {}}
for domain in DOMAIN_ORDER:
if domain == primary:
continue
if domain not in snapshot or domain not in SECTION_BUILDERS:
continue
lines.append("")
lines.append(f"--- {domain.upper()} ---")
lines.extend(SECTION_BUILDERS[domain](snapshot[domain]))
return "\n".join(lines)
+705 -69
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
import json
import os
import time
import uuid
from contextlib import asynccontextmanager
from datetime import datetime, timezone
@@ -12,15 +13,54 @@ from typing import Any
import httpx
import redis.asyncio as aioredis
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from agent_terminal import (
get_all_terminals,
get_terminal_lines,
init_terminals,
make_logger,
set_terminal_publisher,
terminal_log,
)
from lab_context import collect_full_lab_context, format_context_for_agent
from presentation import build_presentation_payload, render_presentation_html
from presentation_upload import get_deck, list_decks, save_upload
from presentation_static import get_static_deck, list_static_decks
from storage_s3 import router as storage_s3_router
from node_registry import NODE_IDS, NODE_AGENT, NODE_REGISTRY, is_node_id
from node_ops import build_node_detail, probe_node, run_node_probe_task
from approval_service import (
APPROVAL_ACTION_TYPES,
approval_stats,
create_approval_request,
decide_approval_request,
detect_agent_proposed_action,
detect_approval_intent,
list_approvals,
)
from db import SessionLocal, db_health, init_database
from supervisor import mirror_terminal_line, mirror_to_supervisors
from workload import build_workload_payload
_workload_cache: dict[str, Any] = {"ts": 0.0, "data": None}
_presentation_cache: dict[str, Any] = {"ts": 0.0, "data": None}
WORKLOAD_CACHE_TTL = 30.0
PRESENTATION_CACHE_TTL = 45.0
from pydantic import BaseModel, Field
from sqlalchemy import Column, DateTime, String, Text, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from sqlalchemy import Column, DateTime, String, Text, select
from sqlalchemy.orm import DeclarativeBase
REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0")
DOCKHAND_URL = os.getenv("DOCKHAND_URL", "http://10.0.21.45:8082")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:////data/atc-agents.db")
GPU_URL = os.getenv("GPU_URL", "http://10.0.20.106:9000")
GPU_UI_URL = os.getenv("GPU_UI_URL", GPU_URL)
LLM_URL = os.getenv("LLM_URL", "http://10.0.20.106:8001/v1")
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o")
LLM_API_KEY = os.getenv("LLM_API_KEY", "sk-local")
LLM_TIMEOUT = float(os.getenv("LLM_TIMEOUT", "120"))
AGENTS = [
{
@@ -29,6 +69,14 @@ AGENTS = [
"color": "#00f0ff",
"zone": "etl",
"role": "Airflow, Kafka, Debezium, S3 pipeline",
"icon": "",
"motto": "Pipelines never sleep",
"capabilities": ["Airflow", "Kafka", "Debezium", "S3", "Connectors"],
"suggested_prompts": [
"How is Debezium doing?",
"Are all Airflow DAGs healthy?",
"Kafka connector status?",
],
},
{
"id": "lakehouse-ops",
@@ -36,6 +84,14 @@ AGENTS = [
"color": "#ff00aa",
"zone": "lakehouse",
"role": "Spark, Trino, Iceberg",
"icon": "🏔️",
"motto": "Query the lake, trust the table",
"capabilities": ["Spark", "Trino", "Iceberg", "Delta", "SQL"],
"suggested_prompts": [
"Lakehouse stack status?",
"Is Trino reachable?",
"How many lakehouse containers are running?",
],
},
{
"id": "data-custodian",
@@ -43,6 +99,14 @@ AGENTS = [
"color": "#ffaa00",
"zone": "db",
"role": "PostgreSQL, MySQL, Mongo, Cassandra, Neo4j",
"icon": "🛡️",
"motto": "Guardian of every row",
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
"suggested_prompts": [
"Hoeveel data zit er in de databases?",
"Wat staat er in PostgreSQL sales_orders?",
"MongoDB supplychain overzicht",
],
},
{
"id": "hadoop-ranger",
@@ -50,13 +114,93 @@ AGENTS = [
"color": "#39ff14",
"zone": "hadoop",
"role": "HDFS, YARN cluster",
"icon": "🌲",
"motto": "Patrol the data forest",
"capabilities": ["HDFS", "YARN", "NameNode", "DataNodes"],
"suggested_prompts": [
"Is HDFS NameNode up?",
"Hadoop cluster status?",
"YARN nodes healthy?",
],
},
{
"id": "infra-sentinel",
"name": "Infra Sentinel",
"color": "#b366ff",
"color": "#9b72cf",
"zone": "docker",
"role": "Docker, Proxmox, monitoring",
"role": "Docker, Proxmox, GPU, monitoring",
"icon": "👁️",
"motto": "See everything, miss nothing",
"capabilities": ["Docker", "Proxmox", "GPU", "vLLM", "Monitoring"],
"suggested_prompts": [
"GPU status?",
"Which LLM model is running?",
"Docker container overview",
],
},
{
"id": "mo-commander",
"name": "Mo · Command",
"color": "#4c9aed",
"zone": "command",
"role": "Supervisor — full event intel, ingress, approvals",
"icon": "🎯",
"motto": "Nothing happens without Mo knowing",
"supervisor": True,
"person": "mo",
"capabilities": ["Events", "Ingress", "Approvals", "Agent dispatch", "Network IN"],
"suggested_prompts": [
"What happened today?",
"What events came in?",
"Pipeline status overview",
],
},
{
"id": "bart-commander",
"name": "Bart · Ops",
"color": "#3fb950",
"zone": "command",
"role": "Supervisor — egress, MCP comms, network OUT",
"icon": "📡",
"motto": "All traffic flows through Bart",
"supervisor": True,
"person": "bart",
"capabilities": ["Egress", "MCP routing", "Network OUT", "GPU inference", "S3 writes"],
"suggested_prompts": [
"What is leaving the cluster?",
"MCP agent communication status?",
"Network egress overview",
],
},
{
"id": "network-watcher",
"name": "Network Watcher",
"color": "#58a6ff",
"zone": "network",
"role": "VLAN 20/21 traffic, data in & out paths",
"icon": "🌐",
"motto": "Every packet tells a story",
"capabilities": ["VLAN 20", "VLAN 21", "Ingress", "Egress", "Firewall paths"],
"suggested_prompts": [
"Data ingress status?",
"What leaves the cluster?",
"Network path to S3?",
],
},
{
"id": "mcp-coordinator",
"name": "MCP Coordinator",
"color": "#f778ba",
"zone": "mcp",
"role": "MCP hub — routes all agent tool calls & comms",
"icon": "🔀",
"motto": "Route once, deliver everywhere",
"capabilities": ["MCP servers", "Tool routing", "Agent relay", "WebSocket bus"],
"suggested_prompts": [
"Which MCP agents are active?",
"MCP hub route status?",
"Agent communication overview",
],
},
]
@@ -69,11 +213,18 @@ ZONES = [
]
INTENT_KEYWORDS: dict[str, list[str]] = {
"data-custodian": ["database", "db", "postgres", "mysql", "mongo", "cassandra", "neo4j", "sql"],
"lakehouse-ops": ["trino", "spark", "lakehouse", "iceberg", "query"],
"hadoop-ranger": ["hadoop", "hdfs", "yarn", "datanode"],
"infra-sentinel": ["docker", "container", "vm", "proxmox", "infra", "grafana"],
"data-custodian": ["database", "postgres", "postgresql", "mysql", "mongo", "mongodb", "cassandra", "neo4j", "sql", "db "],
"lakehouse-ops": ["trino", "spark", "lakehouse", "iceberg", "query", "table"],
"hadoop-ranger": [
"hadoop", "hdfs", "yarn", "datanode", "namenode", "replicatie", "replication",
"rf factor", "opslag", "bestanden", "blocks", "cluster opslag", "data op",
],
"infra-sentinel": ["docker", "container", "vm", "proxmox", "infra", "grafana", "gpu", "vllm", "llm", "nvidia", "inference", "model"],
"etl-guardian": ["airflow", "dag", "debezium", "kafka", "connector", "etl", "pipeline", "s3"],
"network-watcher": ["network", "vlan", "ingress", "egress", "traffic", "packet", "firewall", "route"],
"mcp-coordinator": ["mcp", "tool", "router", "relay", "websocket", "hub"],
"mo-commander": ["mo", "supervisor", "events", "overzicht", "alles", "gebeurd"],
"bart-commander": ["bart", "egress", "uitgaand", "communicatie", "mcp comm"],
}
@@ -100,11 +251,16 @@ class Approval(Base):
action = Column(Text)
reason = Column(Text)
status = Column(String, default="pending")
action_type = Column(String, default="generic.mutate")
target = Column(Text, default="")
payload = Column(Text, default="{}")
decided_by = Column(String, nullable=True)
decide_note = Column(Text, nullable=True)
decided_at = Column(DateTime, nullable=True)
priority = Column(String, default="normal")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(engine)
_db_info = init_database(Base)
redis_client: aioredis.Redis | None = None
ws_clients: set[WebSocket] = set()
@@ -112,14 +268,35 @@ ws_clients: set[WebSocket] = set()
class PromptRequest(BaseModel):
message: str = Field(min_length=1, max_length=2000)
agent_id: str | None = None
class NodeAskRequest(BaseModel):
message: str = Field(min_length=1, max_length=2000)
class ApprovalCreateRequest(BaseModel):
agent_id: str = Field(min_length=1, max_length=64)
action: str = Field(min_length=1, max_length=2000)
reason: str = Field(min_length=1, max_length=2000)
action_type: str = "generic.mutate"
target: str = ""
payload: dict[str, Any] | None = None
priority: str = "normal"
class ApprovalDecision(BaseModel):
approved: bool
decided_by: str = "mo-commander"
note: str = ""
def route_agent(message: str) -> str:
lower = message.lower()
# Storage/data questions default to Hadoop unless clearly about databases
if any(w in lower for w in ("data", "opslag", "gb", "replicatie", "replication", "hdfs", "hadoop")):
if not any(w in lower for w in ("postgres", "mysql", "mongo", "database", "sql", "neo4j", "cassandra")):
return "hadoop-ranger"
scores = {aid: sum(1 for kw in kws if kw in lower) for aid, kws in INTENT_KEYWORDS.items()}
best = max(scores, key=scores.get)
if scores[best] == 0:
@@ -127,6 +304,94 @@ def route_agent(message: str) -> str:
return best
async def gather_agent_context(
agent_id: str,
status: dict[str, Any],
log: Any | None = None,
) -> str:
"""Full lab snapshot for vLLM — all domains, agent's primary domain highlighted."""
snapshot = await collect_full_lab_context(gpu_data=status.get("gpu"), log=log)
snapshot["domains_summary"] = status.get("domains", {})
ctx = format_context_for_agent(agent_id, snapshot)
agent_lines = ["", "=== AGENTS & SUPERVISORS ==="]
for a in AGENTS:
sup = " [supervisor]" if a.get("supervisor") else ""
agent_lines.append(f" - {a['name']} ({a['id']}){sup}: {a['role']}")
ctx = ctx + "\n".join(agent_lines)
if log:
await log("info", "fetch", f"▸ Context assembled: {len(ctx)} chars for LLM")
return ctx
async def ask_llm(
agent_id: str,
message: str,
context: str,
log: Any | None = None,
) -> str | None:
agent = next(a for a in AGENTS if a["id"] == agent_id)
system = f"""Je bent {agent['name']}, een autonomous ops agent in het Dell ATC data lab.
Specialisatie: {agent['role']}.
Motto: {agent.get('motto', '')}
Je antwoordt namens je domein maar hebt zicht op de HELE lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, en GPU/vLLM.
Regels:
- Antwoord in dezelfde taal als de gebruiker (Nederlands of Engels).
- Je hebt volledige zicht op de HELE cluster: alle VMs, zones, connectors, GPU, Hadoop, ObjectScale en Command Center.
- Gebruik ALLEEN de live data hieronder — verzin geen hosts, poorten, cijfers of connector namen.
- Gebruik exact de container/connector namen uit de data (bijv. mysql-hr-connector, niet "Debezium").
- Als iets DOWN of 0 GB is, zeg dat eerlijk.
- Kort en behulpzaam (max ~10 zinnen); bullet lists mogen als het overzicht helpt.
--- LIVE LAB DATA (primary domain eerst, daarna volledige stack) ---
{context}
"""
if log:
await log("info", "llm", f"▸ Querying vLLM model={LLM_MODEL}")
await log("cmd", "llm", f"$ POST {LLM_URL.rstrip('/')}/chat/completions")
await log("info", "llm", f" user: {message[:160]}{'' if len(message) > 160 else ''}")
try:
async with httpx.AsyncClient(timeout=LLM_TIMEOUT) as client:
t0 = time.monotonic()
r = await client.post(
f"{LLM_URL.rstrip('/')}/chat/completions",
headers={
"Authorization": f"Bearer {LLM_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": LLM_MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": message},
],
"max_tokens": 800,
"temperature": 0.25,
},
)
r.raise_for_status()
content = r.json()["choices"][0]["message"]["content"].strip()
ms = int((time.monotonic() - t0) * 1000)
if content and content.strip("!"):
if log:
await log("ok", "llm", f"← vLLM response {len(content)} chars ({ms}ms)")
preview = content.replace("\n", " ")[:180]
await log("info", "llm", f" » {preview}{'' if len(content) > 180 else ''}")
return content
if log:
await log("warn", "llm", f"← Empty or invalid LLM output ({ms}ms)")
except Exception as exc:
if log:
await log("err", "llm", f"✗ vLLM error: {exc}")
return None
def fallback_answer(agent_id: str, context: str) -> str:
agent_name = next(a["name"] for a in AGENTS if a["id"] == agent_id)
return f"**{agent_name}** (offline LLM — ruwe data):\n\n{context}"
async def publish_event(event: dict[str, Any]) -> None:
payload = json.dumps(event, default=str)
if redis_client:
@@ -140,6 +405,21 @@ async def publish_event(event: dict[str, Any]) -> None:
for ws in dead:
ws_clients.discard(ws)
et = event.get("type")
if et == "feed":
entry = event.get("entry") or {}
await mirror_to_supervisors(
entry.get("agent_id", "?"),
entry.get("message", ""),
level=entry.get("level", "info"),
)
elif et == "terminal":
await mirror_terminal_line(event.get("line") or {})
elif et in ("agent_dispatch", "agent_fetch", "agent_return"):
aid = event.get("agent_id", "?")
zone = event.get("zone", "")
await mirror_to_supervisors(aid, f"{et} → zone {zone}", level="info", phase="dispatch")
def add_feed(agent_id: str, message: str, level: str = "info") -> dict:
entry_id = str(uuid.uuid4())[:8]
@@ -175,6 +455,67 @@ async def probe_url(url: str) -> bool:
return False
async def collect_gpu() -> dict[str, Any]:
host = GPU_URL.replace("http://", "").replace("https://", "").split("/")[0]
base = {"ok": False, "host": host, "ui_url": GPU_UI_URL}
try:
async with httpx.AsyncClient(timeout=6.0) as client:
metrics_r, model_r, integration_r = await asyncio.gather(
client.get(f"{GPU_URL}/api/gpu/metrics"),
client.get(f"{GPU_URL}/api/active-model"),
client.get(f"{GPU_URL}/api/integration"),
return_exceptions=True,
)
gpus: list[dict[str, Any]] = []
if isinstance(metrics_r, httpx.Response) and metrics_r.status_code == 200:
current = metrics_r.json().get("current", {})
gpus = [
{
"index": g["index"],
"name": g["name"],
"util_gpu": g.get("util_gpu", 0),
"memory_used_mib": g.get("memory_used_mib", 0),
"memory_total_mib": g.get("memory_total_mib", 0),
"temperature_c": g.get("temperature_c", 0),
"power_w": g.get("power_w", 0),
}
for g in current.get("gpus", [])
]
active_model = None
inference_active = False
vllm_url = None
if isinstance(model_r, httpx.Response) and model_r.status_code == 200:
model_data = model_r.json()
active_model = model_data.get("name")
inference_active = bool(model_data.get("inference_active"))
vllm_url = model_data.get("base_url")
if isinstance(integration_r, httpx.Response) and integration_r.status_code == 200:
integ = integration_r.json()
if not active_model:
active_model = integ.get("active_name")
if not inference_active:
inference_active = bool(integ.get("inference_active"))
if not vllm_url:
vllm_url = integ.get("recommended_base_url")
return {
**base,
"ok": len(gpus) > 0 or inference_active,
"inference_active": inference_active,
"active_model": active_model,
"vllm_url": vllm_url,
"gpu_count": len(gpus),
"gpus": gpus,
}
except Exception as exc:
return {**base, "error": str(exc)}
async def collect_status() -> dict[str, Any]:
db_containers = await dockhand_env_containers(5)
db_running = sum(1 for c in db_containers if c.get("state") == "running")
@@ -201,6 +542,10 @@ async def collect_status() -> dict[str, Any]:
return "warn"
return "down"
gpu = await collect_gpu()
gpu_level = "ok" if gpu.get("ok") and gpu.get("inference_active") else ("warn" if gpu.get("ok") else "down")
gpu_label = gpu.get("active_model") or (f"{gpu.get('gpu_count', 0)} GPUs" if gpu.get("ok") else "offline")
return {
"ts": datetime.now(timezone.utc).isoformat(),
"domains": {
@@ -209,47 +554,112 @@ async def collect_status() -> dict[str, Any]:
"lakehouse": {"level": level(lake_running, lake_total), "label": f"{lake_running}/{lake_total} up", "running": lake_running, "total": lake_total},
"hadoop": {"level": "ok" if hdfs_ok else "warn", "label": "NN up" if hdfs_ok else "NN check"},
"etl": {"level": "ok" if kafka_ok and airflow_ok else "warn", "label": "Kafka+Airflow"},
"gpu": {"level": gpu_level, "label": gpu_label},
},
"gpu": gpu,
"kafka_ok": kafka_ok,
"airflow_ok": airflow_ok,
"hdfs_ok": hdfs_ok,
}
async def _run_agent_task_safe(agent_id: str, message: str, prompt_id: str) -> None:
try:
await run_agent_task(agent_id, message, prompt_id)
except Exception as exc:
agent_name = next((a["name"] for a in AGENTS if a["id"] == agent_id), agent_id)
err = f"Sorry — {agent_name} could not complete your request: {exc}"
await terminal_log(agent_id, f"[{prompt_id}] ✗ Error: {exc}", level="err", phase="error", prompt_id=prompt_id)
feed = add_feed(agent_id, f"{agent_name} failed: {str(exc)[:80]}", "err")
await publish_event({"type": "feed", "entry": feed})
await publish_event({
"type": "prompt_result",
"prompt_id": prompt_id,
"agent_id": agent_id,
"answer": err,
})
async def run_agent_task(agent_id: str, message: str, prompt_id: str) -> str:
zone = next(a["zone"] for a in AGENTS if a["id"] == agent_id)
agent_name = next(a["name"] for a in AGENTS if a["id"] == agent_id)
log = make_logger(agent_id, prompt_id)
approval_created = False
intent = detect_approval_intent(message)
if intent:
with SessionLocal() as db:
await create_approval_request(
db=db,
ApprovalModel=Approval,
agent_id=agent_id,
action=intent["action"],
reason=intent["reason"],
action_type=intent["action_type"],
terminal_log=terminal_log,
mirror_supervisors=mirror_to_supervisors,
publish=publish_event,
add_feed=add_feed,
)
approval_created = True
await terminal_log(
agent_id,
f"[{prompt_id}] Mutating request detected — approval queued for Mo & Bart",
level="warn",
phase="approval",
prompt_id=prompt_id,
)
await terminal_log(
agent_id,
f"[{prompt_id}] ▶ Mission accepted: {message}",
level="info",
phase="dispatch",
prompt_id=prompt_id,
)
await publish_event({"type": "agent_dispatch", "agent_id": agent_id, "zone": zone, "prompt_id": prompt_id})
await asyncio.sleep(0.8)
await asyncio.sleep(0.4)
await terminal_log(agent_id, f"[{prompt_id}] Walking to zone: {zone}", level="info", phase="dispatch", prompt_id=prompt_id)
await publish_event({"type": "agent_fetch", "agent_id": agent_id, "zone": zone, "prompt_id": prompt_id})
await log("info", "fetch", f"[{prompt_id}] Collecting live lab metrics…")
status = await collect_status()
answer_parts = [f"**{next(a['name'] for a in AGENTS if a['id'] == agent_id)}** reporting:"]
context = await gather_agent_context(agent_id, status, log=log)
if agent_id == "data-custodian":
db = status["domains"]["databases"]
containers = await dockhand_env_containers(5)
names = ", ".join(f"{c['name']}:{c.get('state','?')}" for c in containers[:8])
answer_parts.append(f"Databases {db['label']}. Containers: {names or 'unreachable'}.")
elif agent_id == "etl-guardian":
answer_parts.append(
f"Kafka UI: {'OK' if status['kafka_ok'] else 'DOWN'}. "
f"Airflow: {'OK' if status['airflow_ok'] else 'DOWN'}. "
f"Lakehouse {status['domains']['lakehouse']['label']}."
)
elif agent_id == "lakehouse-ops":
answer_parts.append(f"Lakehouse stack {status['domains']['lakehouse']['label']}. Trino at 10.0.21.50:8089.")
elif agent_id == "hadoop-ranger":
answer_parts.append(f"HDFS NameNode: {'reachable' if status['hdfs_ok'] else 'unreachable'} on 10.0.21.61:9870.")
else:
answer_parts.append(
f"Docker {status['domains']['docker']['label']}. "
f"Overall lab health snapshot collected."
)
answer = await ask_llm(agent_id, message, context, log=log)
if not answer:
await log("warn", "llm", "LLM fallback — returning raw context")
answer = fallback_answer(agent_id, context)
answer = " ".join(answer_parts)
await asyncio.sleep(0.6)
if not approval_created:
proposed = detect_agent_proposed_action(answer, message)
if proposed:
with SessionLocal() as db:
await create_approval_request(
db=db,
ApprovalModel=Approval,
agent_id=agent_id,
action=proposed["action"],
reason=proposed["reason"],
action_type=proposed["action_type"],
target=proposed.get("target", ""),
terminal_log=terminal_log,
mirror_supervisors=mirror_to_supervisors,
publish=publish_event,
add_feed=add_feed,
)
approval_created = True
answer = (
f"{answer}\n\n⏸ **Approval required** — this action is in the Approval Inbox. "
f"Mo & Bart have been notified and must approve before we execute."
)
await asyncio.sleep(0.3)
await terminal_log(agent_id, f"[{prompt_id}] ✓ Mission complete", level="ok", phase="done", prompt_id=prompt_id)
await publish_event({"type": "agent_return", "agent_id": agent_id, "zone": zone, "prompt_id": prompt_id})
feed = add_feed(agent_id, f"Prompt answered: {message[:80]}", "info")
feed = add_feed(agent_id, f"{agent_name} completed a response (see Comms)", "info")
await publish_event({"type": "feed", "entry": feed})
await publish_event({"type": "prompt_result", "prompt_id": prompt_id, "agent_id": agent_id, "answer": answer})
return answer
@@ -259,7 +669,9 @@ async def heartbeat_loop() -> None:
while True:
try:
status = await collect_status()
workload = await collect_workload()
await publish_event({"type": "status", "data": status})
await publish_event({"type": "workload", "data": workload})
for domain, info in status["domains"].items():
if info["level"] == "down":
agent = "data-custodian" if domain == "databases" else "infra-sentinel"
@@ -274,6 +686,15 @@ async def heartbeat_loop() -> None:
async def lifespan(app: FastAPI):
global redis_client
redis_client = aioredis.from_url(REDIS_URL, decode_responses=True)
set_terminal_publisher(publish_event)
init_terminals([a["id"] for a in AGENTS] + NODE_IDS)
for a in AGENTS:
await terminal_log(a["id"], f"{a['name']} terminal online — awaiting missions", level="info", phase="boot")
for nid in NODE_IDS:
if nid not in NODE_REGISTRY:
continue
meta = NODE_REGISTRY[nid]
await terminal_log(nid, f"{meta['label']} shell ready — click node to connect", level="info", phase="boot")
task = asyncio.create_task(heartbeat_loop())
add_feed("infra-sentinel", "ATC Command Center API online", "info")
yield
@@ -283,6 +704,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="ATC Command Center API", lifespan=lifespan)
app.include_router(storage_s3_router)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -294,7 +716,103 @@ app.add_middleware(
@app.get("/api/health")
async def health():
return {"ok": True, "ts": datetime.now(timezone.utc).isoformat()}
llm_ok = False
try:
async with httpx.AsyncClient(timeout=4.0) as client:
r = await client.get(f"{LLM_URL.rstrip('/')}/models", headers={"Authorization": f"Bearer {LLM_API_KEY}"})
llm_ok = r.status_code == 200
except Exception:
pass
return {
"ok": True,
"ts": datetime.now(timezone.utc).isoformat(),
"llm_url": LLM_URL,
"llm_ok": llm_ok,
"llm_model": LLM_MODEL,
"database": db_health(),
"db_init": _db_info,
}
async def collect_workload(*, fast: bool = True, use_cache: bool = True) -> dict[str, Any]:
import time as _time
now = _time.time()
if use_cache and _workload_cache.get("data") and now - float(_workload_cache.get("ts") or 0) < WORKLOAD_CACHE_TTL:
return _workload_cache["data"]
gpu = await collect_gpu()
snap = await collect_full_lab_context(gpu_data=gpu, include_inventory=not fast)
payload = build_workload_payload(snap)
_workload_cache["ts"] = now
_workload_cache["data"] = payload
return payload
async def get_presentation_data(*, use_cache: bool = True) -> dict[str, Any]:
import time as _time
now = _time.time()
if use_cache and _presentation_cache.get("data") and now - float(_presentation_cache.get("ts") or 0) < PRESENTATION_CACHE_TTL:
return _presentation_cache["data"]
gpu = await collect_gpu()
snap = await collect_full_lab_context(gpu_data=gpu, include_inventory=False)
data = build_presentation_payload(snap)
data["source"] = "live"
_presentation_cache["ts"] = now
_presentation_cache["data"] = data
return data
@app.get("/api/presentation")
async def get_presentation():
return await get_presentation_data()
@app.get("/api/presentation/html")
async def get_presentation_html():
from fastapi.responses import HTMLResponse
payload = await get_presentation_data()
return HTMLResponse(render_presentation_html(payload))
@app.get("/api/presentation/decks")
async def get_presentation_decks():
return {"live": True, "builtin": list_static_decks(), "uploaded": list_decks()}
@app.get("/api/presentation/decks/{deck_id}")
async def get_presentation_deck(deck_id: str):
if deck_id == "live":
return await get_presentation_data()
deck = get_static_deck(deck_id) or get_deck(deck_id)
if not deck:
return {"error": "deck not found"}
return deck
@app.get("/api/presentation/decks/{deck_id}/html")
async def get_presentation_deck_html(deck_id: str):
from fastapi.responses import HTMLResponse
if deck_id == "live":
payload = await get_presentation_data()
else:
payload = get_static_deck(deck_id) or get_deck(deck_id)
if not payload:
return HTMLResponse("<h1>Deck not found</h1>", status_code=404)
return HTMLResponse(render_presentation_html(payload))
@app.post("/api/presentation/upload")
async def upload_presentation(file: UploadFile = File(...)):
content = await file.read()
if len(content) > 50 * 1024 * 1024:
return {"error": "file too large (max 50MB)"}
deck = await save_upload(file.filename or "upload.pptx", content)
return {"ok": True, "deck": deck}
@app.get("/api/workload")
async def get_workload(fast: bool = True):
return await collect_workload(fast=fast, use_cache=True)
@app.get("/api/status")
@@ -302,9 +820,95 @@ async def get_status():
return await collect_status()
@app.get("/api/gpu")
async def get_gpu():
return await collect_gpu()
def agent_stats() -> dict[str, dict[str, Any]]:
stats: dict[str, dict[str, Any]] = {a["id"]: {"tasks": 0, "last_active": None, "alerts": 0} for a in AGENTS}
with SessionLocal() as db:
rows = db.execute(select(FeedEntry).order_by(FeedEntry.ts.desc()).limit(200)).scalars().all()
for r in rows:
aid = r.agent_id
if aid not in stats:
continue
stats[aid]["tasks"] += 1
if r.level == "warn":
stats[aid]["alerts"] += 1
if stats[aid]["last_active"] is None and r.ts:
stats[aid]["last_active"] = r.ts.isoformat()
return stats
@app.get("/api/agents")
async def get_agents():
return {"agents": AGENTS, "zones": ZONES}
stats = agent_stats()
enriched = [{**a, "stats": stats.get(a["id"], {})} for a in AGENTS]
return {"agents": enriched, "zones": ZONES}
@app.get("/api/terminals")
async def get_terminals(limit: int = 200):
return {"terminals": get_all_terminals(limit)}
@app.get("/api/terminals/{subject_id}")
async def get_subject_terminal(subject_id: str, limit: int = 200):
valid_agents = {a["id"] for a in AGENTS}
if subject_id not in valid_agents and not is_node_id(subject_id):
return {"error": "unknown subject"}
return {"agent_id": subject_id, "lines": get_terminal_lines(subject_id, limit)}
@app.get("/api/nodes")
async def list_nodes():
workload = await collect_workload()
nodes = workload.get("topology", {}).get("nodes", [])
return {"nodes": [{"id": n["id"], "label": n["label"], "ip": n["ip"], "level": n["level"]} for n in nodes]}
@app.get("/api/nodes/{node_id}")
async def get_node(node_id: str):
if not is_node_id(node_id):
return {"error": "unknown node"}
workload = await collect_workload()
wn = next((n for n in workload.get("topology", {}).get("nodes", []) if n["id"] == node_id), None)
gpu = await collect_gpu()
snap = await collect_full_lab_context(gpu_data=gpu)
return build_node_detail(node_id, snap, wn)
@app.post("/api/nodes/{node_id}/probe")
async def post_node_probe(node_id: str):
if not is_node_id(node_id):
return {"error": "unknown node"}
asyncio.create_task(run_node_probe_task(node_id))
return {"ok": True, "node_id": node_id, "status": "probing"}
async def run_node_ask_task(node_id: str, message: str) -> None:
agent_id = NODE_AGENT.get(node_id, "infra-sentinel")
meta = NODE_REGISTRY[node_id]
await terminal_log(node_id, f"▶ Query: {message}", level="info", phase="ask")
await terminal_log(node_id, f"→ Routing to agent {agent_id}", level="info", phase="ask")
log = make_logger(node_id)
status = await collect_status()
context = await gather_agent_context(agent_id, status, log=log)
node_ctx = f"\n\n=== FOCUSED NODE: {meta['label']} ({meta['ip']}) ===\n{meta.get('description', '')}\n"
answer = await ask_llm(agent_id, message, context + node_ctx, log=log)
if not answer:
answer = fallback_answer(agent_id, context)
await terminal_log(node_id, f"{answer}", level="llm", phase="answer")
await publish_event({"type": "node_ask_result", "node_id": node_id, "agent_id": agent_id, "answer": answer})
@app.post("/api/nodes/{node_id}/ask")
async def post_node_ask(node_id: str, body: NodeAskRequest):
if not is_node_id(node_id):
return {"error": "unknown node"}
asyncio.create_task(run_node_ask_task(node_id, body.message))
return {"ok": True, "node_id": node_id, "agent_id": NODE_AGENT.get(node_id), "status": "processing"}
@app.get("/api/feed")
@@ -326,47 +930,73 @@ async def get_feed(limit: int = 50):
@app.get("/api/approvals")
async def get_approvals():
async def get_approvals(status: str = "pending", limit: int = 100):
with SessionLocal() as db:
rows = db.execute(select(Approval).where(Approval.status == "pending")).scalars().all()
return {
"approvals": [
{
"id": r.id,
"ts": r.ts.isoformat() if r.ts else None,
"agent_id": r.agent_id,
"action": r.action,
"reason": r.reason,
"status": r.status,
}
for r in rows
]
}
items = list_approvals(db, Approval, status=status, limit=limit)
stats = approval_stats(db, Approval)
return {"approvals": items, "stats": stats, "action_types": APPROVAL_ACTION_TYPES}
@app.get("/api/approvals/stats")
async def get_approval_stats():
with SessionLocal() as db:
return approval_stats(db, Approval)
@app.post("/api/approvals")
async def post_approval(body: ApprovalCreateRequest):
valid_ids = {a["id"] for a in AGENTS}
if body.agent_id not in valid_ids:
return {"error": "unknown agent_id"}
if body.action_type not in APPROVAL_ACTION_TYPES:
body.action_type = "generic.mutate"
with SessionLocal() as db:
item = await create_approval_request(
db=db,
ApprovalModel=Approval,
agent_id=body.agent_id,
action=body.action,
reason=body.reason,
action_type=body.action_type,
target=body.target,
payload=body.payload,
priority=body.priority,
terminal_log=terminal_log,
mirror_supervisors=mirror_to_supervisors,
publish=publish_event,
add_feed=add_feed,
)
return {"ok": True, "approval": item}
@app.post("/api/approvals/{approval_id}/decide")
async def decide_approval(approval_id: str, body: ApprovalDecision):
valid_supervisors = {"mo-commander", "bart-commander"}
decided_by = body.decided_by if body.decided_by in valid_supervisors else "mo-commander"
with SessionLocal() as db:
row = db.get(Approval, approval_id)
if not row:
return {"error": "not found"}
row.status = "approved" if body.approved else "denied"
db.commit()
agent_id = row.agent_id
action = row.action
msg = f"Approval {'approved' if body.approved else 'denied'}: {action}"
feed = add_feed(agent_id, msg, "info" if body.approved else "warn")
await publish_event({"type": "feed", "entry": feed})
await publish_event({"type": "approval_update", "id": approval_id, "status": row.status})
return {"ok": True, "status": row.status}
item = await decide_approval_request(
db=db,
ApprovalModel=Approval,
approval_id=approval_id,
approved=body.approved,
decided_by=decided_by,
note=body.note,
terminal_log=terminal_log,
publish=publish_event,
add_feed=add_feed,
)
if not item:
return {"error": "not found"}
return {"ok": True, "approval": item}
@app.post("/api/prompt")
async def post_prompt(body: PromptRequest):
prompt_id = str(uuid.uuid4())[:8]
agent_id = route_agent(body.message)
valid_ids = {a["id"] for a in AGENTS}
agent_id = body.agent_id if body.agent_id in valid_ids else route_agent(body.message)
add_feed(agent_id, f"Prompt received: {body.message}", "info")
asyncio.create_task(run_agent_task(agent_id, body.message, prompt_id))
asyncio.create_task(_run_agent_task_safe(agent_id, body.message, prompt_id))
return {"prompt_id": prompt_id, "agent_id": agent_id, "status": "dispatched"}
@@ -376,7 +1006,13 @@ async def ws_ops(websocket: WebSocket):
ws_clients.add(websocket)
try:
status = await collect_status()
workload = await collect_workload()
await websocket.send_text(json.dumps({"type": "status", "data": status}, default=str))
await websocket.send_text(json.dumps({"type": "workload", "data": workload}, default=str))
await websocket.send_text(json.dumps({
"type": "terminal_history",
"terminals": get_all_terminals(150),
}, default=str))
while True:
await websocket.receive_text()
except WebSocketDisconnect:
+186
View File
@@ -0,0 +1,186 @@
"""Live probe + context for topology nodes."""
from __future__ import annotations
import asyncio
from typing import Any
import httpx
from agent_terminal import terminal_log
from lab_context import (
AIRFLOW_URL,
DOCKHAND_URL,
GPU_URL,
HDFS_NN_URL,
KAFKA_CONNECT_URL,
KAFKA_UI_URL,
LAKEHOUSE_HOST,
OBJECTSCALE_URL,
SPARK_UI_URL,
TRINO_URL,
collect_databases,
collect_docker_rack,
collect_etl,
collect_gpu_metrics,
collect_hdfs,
collect_lakehouse,
collect_objectscale,
dockhand_containers,
)
from node_registry import NODE_AGENT, NODE_REGISTRY
async def _log(node_id: str, level: str, phase: str, text: str) -> None:
await terminal_log(node_id, text, level=level, phase=phase)
async def probe_node(node_id: str) -> dict[str, Any]:
"""Run live probes for a topology node; stream output to node terminal."""
meta = NODE_REGISTRY.get(node_id)
if not meta:
return {"error": "unknown node"}
await _log(node_id, "info", "shell", f"═══ Connecting to {meta['label']} ({meta['ip']}) ═══")
await _log(node_id, "cmd", "shell", f"$ probe --node {node_id} --vm {meta['vm']}")
result: dict[str, Any] = {"node_id": node_id, "ok": True}
async with httpx.AsyncClient(timeout=10.0, verify=False) as client:
if node_id == "airflow":
etl = await collect_etl(client)
result["data"] = etl
healthy = etl.get("airflow_healthy")
await _log(node_id, "ok" if healthy else "warn", "shell", f"Airflow scheduler: {'HEALTHY' if healthy else 'DEGRADED'}")
for comp, st in (etl.get("airflow_components") or {}).items():
await _log(node_id, "info", "shell", f" · {comp}: {st}")
elif node_id == "db":
raw = await dockhand_containers(client, 5)
db = await collect_databases(client, raw)
result["data"] = db
await _log(node_id, "ok", "shell", f"DB vault: {db['running']}/{db['total']} containers up")
for engine, items in (db.get("by_engine") or {}).items():
await _log(node_id, "info", "shell", f" {engine}:")
for item in items:
await _log(node_id, "info", "shell", f" - {item}")
elif node_id == "debezium":
etl = await collect_etl(client)
result["data"] = {"connectors": etl.get("connectors")}
await _log(node_id, "ok", "shell", f"Kafka Connect @ {KAFKA_CONNECT_URL}")
for c in etl.get("connectors") or []:
await _log(node_id, "info", "shell", f"{c}")
elif node_id == "kafka":
etl = await collect_etl(client)
result["data"] = {"kafka_ui_ok": etl.get("kafka_ui_ok")}
await _log(node_id, "ok" if etl.get("kafka_ui_ok") else "warn", "shell", f"Kafka UI {KAFKA_UI_URL}: {'UP' if etl.get('kafka_ui_ok') else 'DOWN'}")
await _log(node_id, "info", "shell", f" Broker: 10.0.21.36:9092")
elif node_id == "lakehouse":
raw = await dockhand_containers(client, 9)
lh = await collect_lakehouse(client, raw)
result["data"] = lh
await _log(node_id, "ok", "shell", f"Lakehouse {lh['host']}: {lh['running']}/{lh['total']} containers")
await _log(node_id, "info", "shell", f" Trino {TRINO_URL}: {'UP' if lh.get('trino_ok') else 'DOWN'}")
for c in lh.get("containers") or []:
ports = ",".join(c.get("ports") or []) or "internal"
await _log(node_id, "info", "shell", f" · {c['name']}: {c['state']} ports={ports}")
elif node_id == "s3":
os_data = await collect_objectscale(client)
raw = await dockhand_containers(client, 9)
consumer = next((c for c in raw if "s3-kafka" in f"{c.get('name', '')} {c.get('image', '')}".lower()), None)
result["data"] = {"objectscale": os_data, "consumer": consumer}
await _log(node_id, "ok" if os_data.get("reachable") else "warn", "shell", f"ObjectScale {OBJECTSCALE_URL}: {'UP' if os_data.get('reachable') else 'DOWN'}")
await _log(node_id, "info", "shell", f" Bucket: data @ {os_data.get('host')}:{os_data.get('port')}")
if consumer:
await _log(node_id, "info", "shell", f" s3-kafka-consumer: {consumer.get('state')}")
elif node_id == "docker":
raw = await dockhand_containers(client, 1)
dk = await collect_docker_rack(client, raw)
result["data"] = dk
await _log(node_id, "ok", "shell", f"Docker rack: {dk['running']}/{dk['total']} running")
for c in dk.get("containers") or []:
ports = ",".join(c.get("ports") or []) or "internal"
lvl = "info" if c.get("state") == "running" else "warn"
await _log(node_id, lvl, "shell", f" · {c['name']}: {c['state']} ports={ports}")
elif node_id == "hadoop":
hdfs = await collect_hdfs(client)
result["data"] = hdfs
if hdfs.get("reachable"):
await _log(node_id, "ok", "shell", f"NameNode {HDFS_NN_URL}: UP")
await _log(node_id, "info", "shell", f" Capacity: {hdfs.get('capacity_used_gb')}GB / {hdfs.get('capacity_total_gb')}GB")
await _log(node_id, "info", "shell", f" DataNodes: {hdfs.get('live_datanodes')} live, RF=3")
for dn in hdfs.get("datanodes") or []:
await _log(node_id, "info", "shell", f" · {dn['host']}: {dn['used_gb']}GB used, {dn['blocks']} blocks")
else:
await _log(node_id, "err", "shell", "NameNode unreachable")
elif node_id == "gpu":
gpu = await collect_gpu_metrics(client)
result["data"] = gpu
await _log(node_id, "ok" if gpu.get("ok") else "warn", "shell", f"GPU Lab {GPU_URL}")
await _log(node_id, "info", "shell", f" Model: {gpu.get('active_model')} inference={'ON' if gpu.get('inference_active') else 'OFF'}")
for g in gpu.get("gpus") or []:
await _log(node_id, "info", "shell", f" GPU{g['index']}: {g['util_gpu']:.0f}% VRAM {g['memory_used_mib']:.0f}/{g['memory_total_mib']:.0f} MiB")
elif node_id == "command":
result["data"] = {"agents": 9, "url": "http://10.0.21.33"}
await _log(node_id, "ok", "shell", "Command Center online — 9 agents ready")
await _log(node_id, "info", "shell", " API: http://10.0.21.33/api")
await _log(node_id, "info", "shell", " WebSocket: /api/ws/ops")
elif node_id in ("mo-commander", "bart-commander", "mcp-coordinator", "network-watcher"):
from node_registry import NODE_REGISTRY
meta = NODE_REGISTRY[node_id]
await _log(node_id, "ok", "shell", f"{meta['label']} online — monitoring all agent comms")
await _log(node_id, "info", "shell", meta.get("description", ""))
result["data"] = {"role": meta.get("role")}
await _log(node_id, "ok", "shell", "═══ Probe complete — type a question below ═══")
return result
def build_node_detail(node_id: str, snap: dict[str, Any], workload_node: dict | None = None) -> dict[str, Any]:
"""Rich context payload for a single node."""
meta = dict(NODE_REGISTRY.get(node_id, {}))
if not meta:
return {"error": "unknown node"}
wn = workload_node or {}
agent_id = NODE_AGENT.get(node_id, "infra-sentinel")
detail: dict[str, Any] = {
"id": node_id,
"agent_id": agent_id,
**meta,
"level": wn.get("level", "unknown"),
"running": wn.get("running", 0),
"total": wn.get("total", 0),
"apps": wn.get("apps", []),
"connectors": wn.get("connectors"),
"bucket": wn.get("bucket") or meta.get("bucket"),
"port": wn.get("port"),
"model": wn.get("model"),
"util": wn.get("util"),
"hdfs_used_gb": wn.get("hdfs_used_gb"),
"hdfs_total_gb": wn.get("hdfs_total_gb"),
"trino_ok": wn.get("trino_ok"),
"consumer_ok": wn.get("consumer_ok"),
}
edges = (snap.get("_edges") or []) if False else []
_ = edges # reserved for future edge context from workload
return detail
async def run_node_probe_task(node_id: str) -> None:
try:
await probe_node(node_id)
except Exception as exc:
await _log(node_id, "err", "shell", f"Probe failed: {exc}")
+268
View File
@@ -0,0 +1,268 @@
"""Static registry + helpers for topology node metadata."""
from __future__ import annotations
from typing import Any
NODE_IDS = [
"airflow", "db", "debezium", "kafka", "lakehouse", "s3",
"docker", "hadoop", "gpu", "command",
"mo-commander", "bart-commander", "network-watcher", "mcp-coordinator",
]
NODE_AGENT = {
"airflow": "etl-guardian",
"db": "data-custodian",
"debezium": "etl-guardian",
"kafka": "etl-guardian",
"lakehouse": "lakehouse-ops",
"s3": "lakehouse-ops",
"docker": "infra-sentinel",
"hadoop": "hadoop-ranger",
"gpu": "infra-sentinel",
"command": "infra-sentinel",
}
NODE_REGISTRY: dict[str, dict[str, Any]] = {
"airflow": {
"label": "Airflow",
"vm": "atc-airflow01",
"vmid": 105,
"pve": "pve01",
"ip": "10.0.21.55",
"ssh": "ssh root@10.0.21.55",
"role": "orchestrator",
"color": "#4c9aed",
"description": "Orchestrates DAG generate_data_all_databases — seeds PostgreSQL, MySQL, MongoDB, Cassandra and Neo4j on db02.",
"links": [{"label": "Airflow UI", "url": "http://10.0.21.55:8080"}],
"endpoints": [{"name": "web", "host": "10.0.21.55", "port": "8080", "proto": "http"}],
"commands": ["dag list", "health check", "trigger generate_data_all_databases"],
},
"db": {
"label": "DB Vault",
"vm": "atc-db02",
"vmid": 109,
"pve": "pve01",
"ip": "10.0.21.51",
"ssh": "ssh root@10.0.21.51",
"role": "sources",
"color": "#e8a838",
"description": "Source-of-truth databases for CDC. Debezium connectors capture changes from PostgreSQL sales, MySQL HR, MongoDB supply chain and Cassandra telemetry.",
"links": [{"label": "Dockhand env 5", "url": "http://10.0.21.45:8082"}],
"endpoints": [
{"name": "postgres_sales", "host": "10.0.21.51", "port": "5432", "proto": "tcp"},
{"name": "mysql_hr", "host": "10.0.21.51", "port": "3306", "proto": "tcp"},
{"name": "mongodb_supplychain", "host": "10.0.21.51", "port": "27017", "proto": "tcp"},
{"name": "cassandra_telemetry", "host": "10.0.21.51", "port": "9042", "proto": "tcp"},
{"name": "neo4j_graph", "host": "10.0.21.51", "port": "7687", "proto": "tcp"},
],
"commands": ["list containers", "engine status", "connector sources"],
},
"debezium": {
"label": "Debezium CDC",
"vm": "atc-lake01",
"vmid": 108,
"pve": "pve01",
"ip": "10.0.21.50",
"ssh": "ssh root@10.0.21.50",
"role": "cdc",
"color": "#c77dff",
"description": "Kafka Connect on lake01 runs Debezium connectors — streams row-level changes from source DBs into Kafka topics.",
"links": [{"label": "Kafka Connect", "url": "http://10.0.21.50:8083"}],
"endpoints": [{"name": "kafka-connect", "host": "10.0.21.50", "port": "8083", "proto": "http"}],
"commands": ["list connectors", "connector status", "restart connector"],
},
"kafka": {
"label": "Kafka Bus",
"vm": "atc-kafka01",
"vmid": 113,
"pve": "pve01",
"ip": "10.0.21.36",
"ssh": "ssh root@10.0.21.36",
"role": "bus",
"color": "#4c9aed",
"description": "Central event bus. CDC topics flow from Debezium to s3-kafka-consumer and Spark on the lakehouse.",
"links": [{"label": "Kafka UI", "url": "http://10.0.21.36:9000"}],
"endpoints": [
{"name": "broker", "host": "10.0.21.36", "port": "9092", "proto": "tcp"},
{"name": "kafka-ui", "host": "10.0.21.36", "port": "9000", "proto": "http"},
],
"commands": ["broker health", "list topics", "consumer lag"],
},
"lakehouse": {
"label": "Lakehouse Hub",
"vm": "atc-lake01",
"vmid": 108,
"pve": "pve01",
"ip": "10.0.21.50",
"role": "compute",
"color": "#e05297",
"description": "Spark + Trino + s3-kafka-consumer. Trino federates queries across DB catalogs and Iceberg on ObjectScale S3.",
"links": [
{"label": "Trino", "url": "http://10.0.21.50:8089"},
{"label": "Spark UI", "url": "http://10.0.21.50:8080"},
{"label": "Kafka Connect", "url": "http://10.0.21.50:8083"},
],
"endpoints": [
{"name": "trino", "host": "10.0.21.50", "port": "8089", "proto": "http"},
{"name": "spark-master", "host": "10.0.21.50", "port": "8080", "proto": "http"},
{"name": "spark-submit", "host": "10.0.21.50", "port": "7077", "proto": "tcp"},
],
"commands": ["trino status", "spark workers", "s3 consumer logs"],
},
"s3": {
"label": "ObjectScale S3",
"vm": "atc-objectscale",
"vmid": 100,
"pve": "pve01",
"ip": "10.0.20.111",
"ssh": "ssh root@10.0.20.111",
"role": "storage",
"color": "#d4a017",
"description": "Dell ObjectScale S3-compatible storage. Landing zone for s3-kafka-consumer and Trino Iceberg catalog (bucket: data).",
"links": [{"label": "S3 API", "url": "http://10.0.20.111:9020"}],
"endpoints": [{"name": "s3-api", "host": "10.0.20.111", "port": "9020", "proto": "http"}],
"bucket": "data",
"commands": ["bucket status", "consumer write rate", "iceberg catalog"],
},
"docker": {
"label": "Docker Rack",
"vm": "atc-docker01",
"vmid": 115,
"pve": "pve01",
"ip": "10.0.21.45",
"ssh": "ssh root@10.0.21.45",
"role": "infra",
"color": "#9b72cf",
"description": "Platform services — Homepage, Dockhand, Superset, Forgejo, Gitea proxy and monitoring stack.",
"links": [
{"label": "Homepage", "url": "http://10.0.21.45"},
{"label": "Dockhand", "url": "http://10.0.21.45:8082"},
{"label": "Superset", "url": "http://10.0.21.45:8088"},
],
"endpoints": [{"name": "dockhand", "host": "10.0.21.45", "port": "8082", "proto": "http"}],
"commands": ["container list", "restart service", "resource usage"],
},
"hadoop": {
"label": "Hadoop HDFS",
"vm": "atc-hadoop-m01",
"vmid": 210,
"pve": "pve02",
"ip": "10.0.21.61",
"ssh": "ssh root@10.0.21.61",
"role": "parallel",
"color": "#3fb950",
"description": "9-node HDFS cluster (3 masters + 5 datanodes + edge). Parallel storage layer — separate from CDC→S3 pipeline.",
"links": [{"label": "NameNode UI", "url": "http://10.0.21.61:9870"}],
"endpoints": [
{"name": "namenode", "host": "10.0.21.61", "port": "9870", "proto": "http"},
{"name": "datanodes", "host": "10.0.21.65-69", "port": "9866", "proto": "http"},
],
"commands": ["hdfs dfsadmin -report", "datanode status", "block health"],
},
"gpu": {
"label": "GPU Lab",
"vm": "atc-gpu-dev",
"vmid": 303,
"pve": "atc-gpu",
"ip": "10.0.20.106",
"ssh": "ssh root@10.0.20.106",
"role": "inference",
"color": "#3fb950",
"description": "4× V100 GPU lab. vLLM serves the active model (Llama 3 70B GPTQ) — powers agent reasoning in this Command Center.",
"links": [
{"label": "GPU Lab UI", "url": "http://10.0.20.106:9000"},
{"label": "vLLM API", "url": "http://10.0.20.106:8001/v1"},
],
"endpoints": [
{"name": "gpu-lab", "host": "10.0.20.106", "port": "9000", "proto": "http"},
{"name": "vllm", "host": "10.0.20.106", "port": "8001", "proto": "http"},
],
"commands": ["gpu metrics", "model status", "vram usage"],
},
"command": {
"label": "Command Center",
"vm": "MCP · VM304",
"vmid": 304,
"pve": "atc-gpu",
"ip": "10.0.21.33",
"ssh": "ssh root@10.0.21.33",
"role": "hub",
"color": "#4c9aed",
"description": "ATC Command Center — FastAPI + React + Redis + Postgres + Caddy. Agent hub & approval inbox.",
"links": [
{"label": "Dashboard", "url": "http://10.0.21.33/"},
{"label": "Dockhand env 13", "url": "http://10.0.21.45:8082"},
],
"endpoints": [{"name": "api", "host": "10.0.21.33", "port": "80", "proto": "http"}],
"commands": ["agent status", "cluster snapshot", "dispatch mission"],
},
"mo-commander": {
"label": "Mo · Command",
"vm": "Supervisor Desk",
"vmid": 304,
"pve": "atc-gpu",
"ip": "10.0.21.33",
"role": "supervisor",
"color": "#4c9aed",
"description": "Mo's command desk — receives ALL lab events, agent dispatch, ingress traffic, approvals.",
"links": [{"label": "Command Center", "url": "http://10.0.21.33/"}],
"endpoints": [{"name": "intel-feed", "host": "10.0.21.33", "port": "80", "proto": "ws"}],
"commands": ["events today", "ingress log", "agent status"],
},
"bart-commander": {
"label": "Bart · Ops",
"vm": "Supervisor Desk",
"vmid": 304,
"pve": "atc-gpu",
"ip": "10.0.21.33",
"role": "supervisor",
"color": "#3fb950",
"description": "Bart's ops desk — egress monitoring, MCP agent comms, S3 writes, GPU inference output.",
"links": [{"label": "Command Center", "url": "http://10.0.21.33/"}],
"endpoints": [{"name": "egress-feed", "host": "10.0.21.33", "port": "80", "proto": "ws"}],
"commands": ["egress log", "mcp comms", "s3 write rate"],
},
"network-watcher": {
"label": "Network Watcher",
"vm": "multi-VLAN",
"ip": "10.0.20/21.x",
"role": "network",
"color": "#58a6ff",
"description": "Monitors VLAN 20 (storage/GPU) and VLAN 21 (compute) — data ingress and egress paths.",
"links": [],
"endpoints": [
{"name": "vlan20", "host": "10.0.20.0/24", "port": "-", "proto": "net"},
{"name": "vlan21", "host": "10.0.21.0/24", "port": "-", "proto": "net"},
],
"commands": ["ingress paths", "egress paths", "vlan status"],
},
"mcp-coordinator": {
"label": "MCP Coordinator",
"vm": "VM304",
"ip": "10.0.21.33",
"role": "mcp",
"color": "#f778ba",
"description": "Routes all MCP agent tool calls. Relays comms between operational agents and supervisor desks.",
"links": [{"label": "API", "url": "http://10.0.21.33/api"}],
"endpoints": [{"name": "mcp-hub", "host": "10.0.21.33", "port": "3101-3112", "proto": "http"}],
"commands": ["agent routes", "mcp status", "relay log"],
},
}
NODE_AGENT.update({
"mo-commander": "mo-commander",
"bart-commander": "bart-commander",
"network-watcher": "network-watcher",
"mcp-coordinator": "mcp-coordinator",
"etl-guardian": "etl-guardian",
"lakehouse-ops": "lakehouse-ops",
"data-custodian": "data-custodian",
"hadoop-ranger": "hadoop-ranger",
"infra-sentinel": "infra-sentinel",
})
def is_node_id(subject_id: str) -> bool:
return subject_id in NODE_REGISTRY
+292
View File
@@ -0,0 +1,292 @@
"""Build live presentation deck from cluster snapshot + registry."""
from __future__ import annotations
import json
from typing import Any
from node_registry import NODE_REGISTRY
from topology_views import build_all_topologies
from workload import build_workload_payload
def _status_badge(level: str) -> str:
return {"ok": "● Online", "warn": "◐ Degraded", "down": "○ Offline", "unknown": "? Unknown"}.get(level, level)
def _slide(slide_id: str, title: str, subtitle: str, bullets: list[str], **extra: Any) -> dict[str, Any]:
return {"id": slide_id, "title": title, "subtitle": subtitle, "bullets": bullets, **extra}
def build_presentation_payload(snap: dict[str, Any]) -> dict[str, Any]:
workload = build_workload_payload(snap)
topologies = workload.get("topologies") or build_all_topologies(snap)
totals = workload.get("totals", {})
zones = workload.get("zones", [])
gpu = workload.get("gpu", {})
etl = snap.get("etl", {})
hadoop = snap.get("hadoop", {})
objectscale = snap.get("objectscale", {})
command = snap.get("command_center", {})
slides: list[dict[str, Any]] = []
slides.append(_slide(
"title",
"Dell ATC Data Lab",
"Live demo & presentation — Command Center",
[
f"Snapshot: {snap.get('ts', 'now')}",
f"Pipeline: {'ACTIVE' if totals.get('pipeline_active') else 'INACTIVE'}",
f"Apps running: {totals.get('apps_running', 0)}/{totals.get('apps_total', 0)}",
f"CDC connectors: {totals.get('connectors', 0)}",
f"LLM: {gpu.get('model') or 'offline'} ({gpu.get('gpu_count', 0)}× V100)",
"Command Center → http://10.0.21.33/",
],
kind="hero",
))
slides.append(_slide(
"mission",
"Mission",
"End-to-end modern data platform on Dell infrastructure",
[
"Ingest change data from operational databases (PostgreSQL, MySQL, MongoDB, Cassandra)",
"Stream via Kafka & Debezium into the lakehouse (Spark, Trino, Iceberg)",
"Land curated data on ObjectScale S3 — query with Trino & visualize in Superset",
"Parallel HDFS cluster for batch / legacy workloads",
"GPU lab powers autonomous ops agents with local vLLM inference",
"This dashboard orchestrates agents, approvals, and live cluster visibility",
],
kind="narrative",
))
arch = topologies.get("architecture") or workload.get("topology") or {}
arch_nodes = arch.get("nodes", [])
slides.append(_slide(
"architecture",
"Data Platform Architecture",
arch.get("subtitle", "Sources → Ingestion → Compute → Storage → Consumers"),
[f"{n.get('label', n.get('id'))}: {n.get('subtitle', n.get('role', ''))}" for n in arch_nodes[:14]],
kind="topology",
topology=arch,
))
pipeline = topologies.get("pipeline", {})
connector_lines = [
f" · {cs['name']}: {cs.get('state', '?')}"
for cs in (etl.get("connector_status") or [])[:6]
]
slides.append(_slide(
"pipeline",
"CDC Pipeline",
pipeline.get("subtitle", "Airflow → DB → Debezium → Kafka → Lakehouse → S3"),
[
f"Airflow: {'healthy' if etl.get('airflow_healthy') else 'degraded'} ({etl.get('airflow_url', '')})",
f"Kafka UI: {'UP' if etl.get('kafka_ui_ok') else 'DOWN'}",
f"Connectors: {', '.join(etl.get('connectors') or []) or 'none'}",
*connector_lines,
f"Spark UI: {'UP' if etl.get('spark_ui_ok') else 'DOWN'}",
f"ObjectScale: {'reachable' if objectscale.get('reachable') else 'down'} bucket={objectscale.get('bucket', 'data')}",
],
kind="topology",
topology=pipeline,
))
for zone in zones:
apps = zone.get("apps") or []
app_lines = [
f"{a['name']}: {a['state']}" + (f" ({a.get('host', '')})" if a.get("host") else "")
for a in apps[:10]
]
slides.append(_slide(
f"zone-{zone['id']}",
zone["label"],
f"{zone.get('vm', '')} · {zone.get('ip', '')} · {_status_badge(zone.get('level', 'unknown'))}",
[
f"Containers: {zone.get('running', 0)}/{zone.get('total', 0)} running",
*app_lines,
],
kind="zone",
zone=zone,
))
infra_nodes = [
nid for nid in NODE_REGISTRY
if nid not in ("mo-commander", "bart-commander", "network-watcher", "mcp-coordinator")
]
slides.append(_slide(
"infrastructure",
"Infrastructure Map",
"Proxmox VMs & services across VLAN 20/21",
[
f"{NODE_REGISTRY[nid]['label']}{NODE_REGISTRY[nid].get('vm')} "
f"(VMID {NODE_REGISTRY[nid].get('vmid', '?')}) @ {NODE_REGISTRY[nid].get('ip')}"
for nid in infra_nodes
],
kind="registry",
))
dn_lines = [
f" · {dn['host']}: {dn.get('used_gb', 0)} GB — {dn.get('state', '')}"
for dn in (hadoop.get("datanodes") or [])[:5]
]
slides.append(_slide(
"hadoop",
"Hadoop HDFS",
"9-node parallel storage cluster",
[
f"NameNode: {'UP' if hadoop.get('reachable') else 'DOWN'}{hadoop.get('namenode', '')}",
f"Capacity: {hadoop.get('capacity_used_gb', '?')} / {hadoop.get('capacity_total_gb', '?')} GB",
f"DataNodes: {hadoop.get('live_datanodes', 0)} live, {hadoop.get('dead_datanodes', 0)} dead",
f"Files: {hadoop.get('files_total', 0)}, Blocks: {hadoop.get('blocks_total', 0)}",
*dn_lines,
],
kind="data",
))
gpus = gpu.get("gpus") or snap.get("gpu", {}).get("gpus") or []
gpu_lines = [
f"GPU{g['index']}: {g.get('util_gpu', 0):.0f}% util, "
f"{g.get('memory_used_mib', 0):.0f}/{g.get('memory_total_mib', 0):.0f} MiB"
for g in gpus[:4]
]
slides.append(_slide(
"gpu",
"GPU Lab & GenAI",
f"{gpu.get('model') or 'vLLM'} on atc-gpu-dev (VM 303)",
[
f"Inference: {'ON' if gpu.get('inference_active') else 'OFF'}",
f"API: {snap.get('gpu', {}).get('vllm_url') or 'http://10.0.20.106:8001/v1'}",
"Manager: http://10.0.20.106:9000",
*gpu_lines,
],
kind="gpu",
))
slides.append(_slide(
"agents",
"Autonomous Agents",
"Mo & Bart supervise 5 domain operators + MCP hub",
[
"ETL Guardian — Airflow, Kafka, Debezium, connectors",
"Data Custodian — PostgreSQL, MySQL, MongoDB, Cassandra, Neo4j",
"Lakehouse Ops — Spark, Trino, Iceberg, ObjectScale S3",
"Hadoop Ranger — HDFS NameNode, DataNodes, block health",
"Infra Sentinel — Docker rack, GPU lab, Command Center",
"All agents receive LIVE cluster snapshot in every LLM prompt",
],
kind="agents",
))
cc_apps = [f"{c['name']}: {c['state']}" for c in (command.get("containers") or [])]
slides.append(_slide(
"command",
"Command Center",
"VM 304 — this presentation runs here",
[
f"Host: {command.get('host', '10.0.21.33')} (VMID {command.get('vmid', 304)})",
f"Stack: {command.get('running', 0)}/{command.get('total', 0)} containers",
*cc_apps,
"WebSocket ops feed · Approval inbox · Agent terminals",
],
kind="command",
))
slides.append(_slide(
"demo",
"Live Demo Tips",
"Use this deck during customer presentations",
[
"Press ← → or click dots to navigate slides",
"F = fullscreen presentation mode",
"Export HTML opens a standalone deck for projectors / offline",
"Ask agents in the Command Bar — they see full cluster context",
"Switch to Data Platform tab for interactive topology",
"GPU Lab chat: http://10.0.20.106:9000/chat",
],
kind="cta",
))
return {
"ts": snap.get("ts"),
"title": "Dell ATC Data Lab",
"subtitle": "Live Infrastructure Presentation",
"totals": totals,
"pipeline_active": totals.get("pipeline_active"),
"slides": slides,
"slide_count": len(slides),
"workload": workload,
"topologies": topologies,
}
def render_presentation_html(payload: dict[str, Any]) -> str:
slides_json = json.dumps(payload.get("slides", []), default=str)
title = payload.get("title", "ATC Lab")
ts = payload.get("ts", "")
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>{title} — Presentation</title>
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:#0c1929;color:#e8f1ff;height:100vh;overflow:hidden}}
.deck{{height:100vh;display:flex;flex-direction:column}}
header{{padding:1rem 2rem;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(96,165,250,.2);background:rgba(15,27,46,.9)}}
header h1{{font-size:1.1rem;font-weight:600}}
header .meta{{font-size:.75rem;opacity:.7}}
.slide{{flex:1;display:none;padding:3rem 4rem;overflow:auto}}
.slide.active{{display:flex;flex-direction:column;justify-content:center}}
.slide h2{{font-size:2.4rem;margin-bottom:.5rem;background:linear-gradient(90deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent}}
.slide h3{{font-size:1rem;opacity:.75;margin-bottom:2rem;font-weight:400}}
.slide ul{{list-style:none;font-size:1.15rem;line-height:1.9}}
.slide li::before{{content:"";color:#60a5fa}}
.slide.hero h2{{font-size:3.2rem}}
nav{{display:flex;gap:.5rem;padding:1rem 2rem;border-top:1px solid rgba(96,165,250,.2);align-items:center}}
nav button{{background:#1e3a5f;border:1px solid rgba(96,165,250,.3);color:#e8f1ff;padding:.5rem 1rem;border-radius:6px;cursor:pointer}}
nav button:hover{{background:#234876}}
.dots{{display:flex;gap:6px;flex:1;justify-content:center;flex-wrap:wrap}}
.dot{{width:8px;height:8px;border-radius:50%;background:rgba(96,165,250,.3);cursor:pointer;border:none}}
.dot.active{{background:#60a5fa;transform:scale(1.3)}}
.counter{{font-size:.8rem;opacity:.6;min-width:4rem;text-align:right}}
</style>
</head>
<body>
<div class="deck">
<header><h1>{title}</h1><div class="meta">Dell ATC · Live snapshot {ts}</div></header>
<div id="slides"></div>
<nav>
<button id="prev">← Prev</button>
<div class="dots" id="dots"></div>
<button id="next">Next →</button>
<span class="counter" id="counter"></span>
</nav>
</div>
<script>
const slides={slides_json};
let i=0;
const container=document.getElementById("slides");
const dots=document.getElementById("dots");
const counter=document.getElementById("counter");
slides.forEach((s,idx)=>{{
const el=document.createElement("section");
el.className="slide"+(s.kind==="hero"?" hero":"")+(idx===0?" active":"");
const bullets=(s.bullets||[]).map(b=>"<li>"+b+"</li>").join("");
el.innerHTML="<h2>"+s.title+"</h2><h3>"+(s.subtitle||"")+"</h3><ul>"+bullets+"</ul>";
container.appendChild(el);
const d=document.createElement("button");
d.className="dot"+(idx===0?" active":"");
d.onclick=()=>go(idx);
dots.appendChild(d);
}});
function go(n){{i=Math.max(0,Math.min(slides.length-1,n));document.querySelectorAll(".slide").forEach((e,j)=>e.classList.toggle("active",j===i));document.querySelectorAll(".dot").forEach((e,j)=>e.classList.toggle("active",j===i));counter.textContent=(i+1)+"/"+slides.length;}}
document.getElementById("prev").onclick=()=>go(i-1);
document.getElementById("next").onclick=()=>go(i+1);
document.onkeydown=e=>{{if(e.key==="ArrowRight"||e.key===" ")go(i+1);if(e.key==="ArrowLeft")go(i-1);if(e.key==="f"||e.key==="F")document.documentElement.requestFullscreen?.();}};
go(0);
</script>
</body>
</html>"""
+203
View File
@@ -0,0 +1,203 @@
"""Pre-built modern HTML presentation templates — English."""
from __future__ import annotations
from typing import Any
MODERN_DECKS: dict[str, dict[str, Any]] = {
"data-maturity": {
"id": "data-maturity",
"title": "Data Maturity Assessment",
"subtitle": "Dell ATC — Customer Data Onboarding Framework",
"slides": [
{
"id": "dm-1", "kind": "hero",
"title": "Data Maturity Assessment",
"subtitle": "From raw data to trusted decisions",
"bullets": [
"6 dimensions: Completeness, Consistency, Validity, Uniqueness, Timeliness, Accuracy",
"Automated analysis with Docling, Great Expectations & Soda Core",
"Full report with priorities and remediation roadmap",
],
},
{
"id": "dm-2", "kind": "narrative",
"title": "Why maturity?",
"subtitle": "Customers hand over data — we show where it stands",
"bullets": [
"73% of analytics projects fail due to data quality (Gartner)",
"Without a baseline there is no measurable improvement",
"DQ tools + document parsing = complete picture",
"Report ready for boardroom & audit",
],
},
{
"id": "dm-3", "kind": "zone",
"title": "Our toolchain",
"subtitle": "Integrated on the ATC platform",
"bullets": [
"Docling — PDF, PPTX, DOCX, XLSX → structured data",
"Great Expectations — Python expectations & data contracts",
"Soda Core — YAML checks, freshness, anomaly monitoring",
"DQ API — maturity score + HTML report",
],
},
{
"id": "dm-4", "kind": "cta",
"title": "Next step",
"subtitle": "Upload customer data in Data Quality tab",
"bullets": [
"Upload CSV, Excel, PDF or database export",
"Receive maturity score (0100) per dimension",
"Action list: what to fix first",
"Reports are stored — no need to re-upload",
],
},
],
},
"atc-platform": {
"id": "atc-platform",
"title": "ATC Data Platform",
"subtitle": "Modern lakehouse on Dell infrastructure",
"slides": [
{
"id": "atc-1", "kind": "hero",
"title": "ATC Data & AI Platform",
"subtitle": "CDC → Kafka → Spark → Iceberg → Trino",
"bullets": [
"Live pipeline: PostgreSQL, MySQL, MongoDB, Cassandra",
"ObjectScale S3 + 9-node Hadoop cluster",
"GPU Lab: Llama 3 70B for autonomous agents",
],
},
{
"id": "atc-2", "kind": "topology",
"title": "End-to-end flow",
"subtitle": "Sources → Ingestion → Compute → Storage → Consumers",
"bullets": [
"Airflow orchestrates daily data generation",
"Debezium CDC → Kafka → Spark → Iceberg",
"Trino federated queries + Superset BI",
"GenAI agents with full cluster context",
],
},
],
},
"stack-architecture": {
"id": "stack-architecture",
"title": "ATC Stack Architecture",
"subtitle": "How the Command Center, DQ, RAG & Lakehouse fit together",
"slides": [
{
"id": "arch-1", "kind": "hero",
"title": "ATC Intelligent Data Platform",
"subtitle": "One dashboard — ingest, assess, chat, present",
"bullets": [
"Command Center at http://10.0.21.33 — single entry point",
"Upload once → stored permanently in ChromaDB + file registry",
"Ask questions anytime via Knowledge Chat (RAG + LangChain)",
"Present architecture & maturity to customers live",
],
},
{
"id": "arch-2", "kind": "architecture", "animation": "full-stack",
"title": "Full Stack Overview",
"subtitle": "All services on VM304 (Command Center)",
"bullets": [
"Caddy routes /api, /dq, /rag to backend services",
"React UI — Data Platform, Presentation, Data Quality, Knowledge Chat",
"Docling on port 5001 for document parsing UI + API",
"Postgres + Redis for agents; ChromaDB for vectors",
],
},
{
"id": "arch-3", "kind": "architecture", "animation": "lakehouse",
"title": "Lakehouse Pipeline",
"subtitle": "Operational data → analytics-ready tables",
"bullets": [
"Sources on DB Vault (10.0.21.51): PG, MySQL, Mongo, Cassandra, Neo4j",
"Debezium captures changes → Kafka topics",
"Spark transforms → Iceberg tables on ObjectScale",
"Trino SQL + Superset dashboards for consumers",
],
},
{
"id": "arch-4", "kind": "architecture", "animation": "dq-flow",
"title": "Data Quality & Maturity",
"subtitle": "Prove data readiness before AI/ML projects",
"bullets": [
"Upload customer file → parsed by Docling if PDF/Office",
"6 maturity dimensions scored 0100 with findings",
"GE + Soda checks per column — expandable in UI",
"HTML report + image gallery — stored in /data/reports",
],
},
{
"id": "arch-5", "kind": "architecture", "animation": "rag-flow",
"title": "Knowledge Chat (RAG)",
"subtitle": "Upload once — query forever",
"bullets": [
"Document saved to disk + indexed in ChromaDB (persistent volume)",
"Duplicate uploads skipped automatically (SHA-256 hash)",
"LangChain retrieves top-k chunks → Llama 70B on GPU Lab",
"Answers include source filename + chunk preview",
],
},
{
"id": "arch-6", "kind": "narrative",
"title": "AI Agents Layer",
"subtitle": "Autonomous ops with full lab context",
"bullets": [
"Supervisor + field operators on Command Center",
"Each agent sees live workload, GPU, databases, topology",
"LLM: Llama 3 70B GPTQ via vLLM (10.0.20.106:8001)",
"Approval workflow for sensitive operations",
],
},
{
"id": "arch-7", "kind": "zone",
"title": "Infrastructure Map",
"subtitle": "Dell ATC cluster — key IPs",
"bullets": [
"Command Center VM304: 10.0.21.33 (this dashboard)",
"GPU Lab VM303: 10.0.20.106 — 7× V100, vLLM, model manager",
"DB Vault: 10.0.21.51 · Lakehouse: 10.0.21.50",
"Docling UI: http://10.0.21.33:5001/ui/",
],
},
{
"id": "arch-8", "kind": "cta",
"title": "Customer Demo Flow",
"subtitle": "Recommended narrative for presentations",
"bullets": [
"1. Show live Data Platform topology & agent fleet",
"2. Upload customer sample → Data Quality maturity report",
"3. Same file already in Knowledge Chat — ask questions live",
"4. Export this architecture deck as HTML for customer handout",
],
},
],
},
}
def list_static_decks() -> list[dict[str, Any]]:
return [
{"id": k, "title": v["title"], "subtitle": v["subtitle"], "slide_count": len(v["slides"]), "source": "builtin"}
for k, v in MODERN_DECKS.items()
]
def get_static_deck(deck_id: str) -> dict[str, Any] | None:
deck = MODERN_DECKS.get(deck_id)
if not deck:
return None
return {
"ts": None,
"title": deck["title"],
"subtitle": deck["subtitle"],
"slides": deck["slides"],
"slide_count": len(deck["slides"]),
"source": "builtin",
"id": deck_id,
}
+165
View File
@@ -0,0 +1,165 @@
"""Upload PPT/PPTX decks and convert to presentation JSON + HTML."""
from __future__ import annotations
import json
import os
import re
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import httpx
PRESENTATIONS_DIR = Path(os.getenv("PRESENTATIONS_DIR", "/data/presentations"))
DOCLING_URL = os.getenv("DOCLING_URL", "http://docling-serve:5001").rstrip("/")
def _ensure_dir() -> Path:
PRESENTATIONS_DIR.mkdir(parents=True, exist_ok=True)
return PRESENTATIONS_DIR
def list_decks() -> list[dict[str, Any]]:
_ensure_dir()
decks = []
for meta_path in sorted(PRESENTATIONS_DIR.glob("*/meta.json"), key=lambda p: p.stat().st_mtime, reverse=True):
try:
meta = json.loads(meta_path.read_text())
decks.append(meta)
except Exception:
continue
return decks
def _safe_name(name: str) -> str:
return re.sub(r"[^a-zA-Z0-9._-]+", "_", name)[:80]
def pptx_to_slides(path: Path) -> list[dict[str, Any]]:
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
prs = Presentation(str(path))
slides: list[dict[str, Any]] = []
for idx, slide in enumerate(prs.slides, start=1):
bullets: list[str] = []
title = ""
for shape in slide.shapes:
if not hasattr(shape, "text"):
continue
text = (shape.text or "").strip()
if not text:
continue
if shape.shape_type == MSO_SHAPE_TYPE.PLACEHOLDER and not title:
title = text.split("\n")[0][:120]
else:
for line in text.split("\n"):
line = line.strip()
if line and line != title:
bullets.append(line[:240])
if not title:
title = f"Slide {idx}"
slides.append({
"id": f"upload-{idx}",
"title": title,
"subtitle": "",
"bullets": bullets[:12] or ["(empty slide)"],
"kind": "upload",
})
return slides
async def docling_enrich(path: Path) -> list[dict[str, Any]] | None:
"""Optional: parse via Docling for richer structure."""
try:
async with httpx.AsyncClient(timeout=120.0) as client:
with path.open("rb") as f:
r = await client.post(
f"{DOCLING_URL}/v1/convert/file",
files={"files": (path.name, f, "application/octet-stream")},
data={"to_formats": "md"},
)
if r.status_code >= 400:
return None
data = r.json()
md = ""
if isinstance(data, dict):
doc = data.get("document") or data.get("result") or data
if isinstance(doc, dict):
md = doc.get("md_content") or doc.get("markdown") or ""
elif isinstance(doc, str):
md = doc
if not md:
return None
slides = []
chunks = [c.strip() for c in re.split(r"\n#{1,2}\s+", md) if c.strip()]
for i, chunk in enumerate(chunks[:40], start=1):
lines = [ln.strip() for ln in chunk.split("\n") if ln.strip()]
title = lines[0][:120] if lines else f"Slide {i}"
bullets = [ln.lstrip("-•* ").strip() for ln in lines[1:13] if ln.strip()]
slides.append({
"id": f"docling-{i}",
"title": title,
"subtitle": "Docling parsed",
"bullets": bullets or [""],
"kind": "upload",
})
return slides if slides else None
except Exception:
return None
async def save_upload(filename: str, content: bytes) -> dict[str, Any]:
_ensure_dir()
deck_id = str(uuid.uuid4())[:8]
deck_dir = PRESENTATIONS_DIR / deck_id
deck_dir.mkdir(parents=True, exist_ok=True)
safe = _safe_name(filename)
dest = deck_dir / safe
dest.write_bytes(content)
slides: list[dict[str, Any]] = []
source = "pptx"
if safe.lower().endswith((".pptx", ".ppt")):
slides = pptx_to_slides(dest)
docling_slides = await docling_enrich(dest)
if docling_slides and len(docling_slides) >= len(slides):
slides = docling_slides
source = "docling+pptx"
else:
docling_slides = await docling_enrich(dest)
if docling_slides:
slides = docling_slides
source = "docling"
if not slides:
slides = [{
"id": "upload-1",
"title": safe,
"subtitle": "Uploaded file",
"bullets": [f"File stored at {dest.name}", "Could not auto-parse slides — open in editor or re-upload PPTX"],
"kind": "upload",
}]
payload = {
"id": deck_id,
"filename": safe,
"source": source,
"ts": datetime.now(timezone.utc).isoformat(),
"title": safe.rsplit(".", 1)[0],
"subtitle": "Uploaded presentation",
"slide_count": len(slides),
"slides": slides,
}
(deck_dir / "meta.json").write_text(json.dumps(payload, indent=2, default=str))
(deck_dir / "deck.json").write_text(json.dumps(payload, default=str))
return payload
def get_deck(deck_id: str) -> dict[str, Any] | None:
path = PRESENTATIONS_DIR / deck_id / "meta.json"
if not path.exists():
return None
return json.loads(path.read_text())
+7
View File
@@ -4,6 +4,13 @@ redis==5.2.1
httpx==0.28.1
sqlalchemy==2.0.36
aiosqlite==0.20.0
psycopg2-binary==2.9.10
pydantic==2.10.4
python-multipart==0.0.20
websockets==14.1
pymysql==1.1.1
pymongo==4.10.1
cassandra-driver==3.29.2
neo4j==5.26.0
python-pptx==1.0.2
boto3==1.35.99
+138
View File
@@ -0,0 +1,138 @@
"""ObjectScale / S3 storage API for Command Center."""
from __future__ import annotations
import os
from typing import Any
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
from fastapi import APIRouter, Query
from fastapi.responses import JSONResponse, StreamingResponse
S3_ENDPOINT = os.getenv("S3_ENDPOINT", "http://10.0.20.111:9020")
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "object_admin1")
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "ChangeMeChangeMeChangeMeChangeMeChangeMe")
S3_REGION = os.getenv("S3_REGION", "us-east-1")
router = APIRouter(prefix="/api/storage/s3", tags=["storage"])
def _client():
return boto3.client(
"s3",
endpoint_url=S3_ENDPOINT,
aws_access_key_id=S3_ACCESS_KEY,
aws_secret_access_key=S3_SECRET_KEY,
region_name=S3_REGION,
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
)
def _human_size(n: int) -> str:
for unit in ("B", "KB", "MB", "GB", "TB"):
if n < 1024:
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} PB"
@router.get("/health")
async def s3_health():
try:
s3 = _client()
buckets = s3.list_buckets()
names = [b["Name"] for b in buckets.get("Buckets", [])]
return {
"ok": True,
"endpoint": S3_ENDPOINT,
"buckets": len(names),
"bucket_names": names,
}
except Exception as exc:
return JSONResponse({"ok": False, "endpoint": S3_ENDPOINT, "error": str(exc)}, status_code=502)
@router.get("/buckets")
async def list_buckets():
try:
s3 = _client()
resp = s3.list_buckets()
items = []
for b in resp.get("Buckets", []):
name = b["Name"]
try:
loc = s3.list_objects_v2(Bucket=name, MaxKeys=1)
count_hint = loc.get("KeyCount", 0)
except ClientError:
count_hint = None
items.append({
"name": name,
"created": b.get("CreationDate", "").isoformat() if b.get("CreationDate") else None,
"has_objects": bool(count_hint),
})
return {"ok": True, "buckets": items, "endpoint": S3_ENDPOINT}
except Exception as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
@router.get("/buckets/{bucket}/objects")
async def list_objects(
bucket: str,
prefix: str = Query("", alias="prefix"),
max_keys: int = Query(200, le=500),
):
try:
s3 = _client()
resp = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter="/", MaxKeys=max_keys)
folders = [
{"type": "prefix", "name": p["Prefix"][len(prefix):].rstrip("/"), "prefix": p["Prefix"]}
for p in resp.get("CommonPrefixes", [])
]
objects = [
{
"type": "object",
"key": o["Key"],
"name": o["Key"][len(prefix):] if o["Key"].startswith(prefix) else o["Key"],
"size": o.get("Size", 0),
"size_human": _human_size(o.get("Size", 0)),
"modified": o.get("LastModified", "").isoformat() if o.get("LastModified") else None,
}
for o in resp.get("Contents", [])
if o["Key"] != prefix
]
return {
"ok": True,
"bucket": bucket,
"prefix": prefix,
"folders": folders,
"objects": objects,
"truncated": resp.get("IsTruncated", False),
}
except ClientError as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=403)
except Exception as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=502)
@router.get("/buckets/{bucket}/download")
async def download_object(bucket: str, key: str = Query(...)):
try:
s3 = _client()
obj = s3.get_object(Bucket=bucket, Key=key)
body = obj["Body"]
filename = key.split("/")[-1] or "download"
media = obj.get("ContentType") or "application/octet-stream"
def stream():
while chunk := body.read(1024 * 256):
yield chunk
return StreamingResponse(
stream(),
media_type=media,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
except ClientError as exc:
return JSONResponse({"ok": False, "error": str(exc)}, status_code=404)
+34
View File
@@ -0,0 +1,34 @@
"""Fan-out lab events to supervisor agents Mo & Bart."""
from __future__ import annotations
from agent_terminal import terminal_log
from node_registry import NODE_IDS
SUPERVISOR_IDS = ["mo-commander", "bart-commander"]
OPERATOR_IDS = {
"etl-guardian", "lakehouse-ops", "data-custodian", "hadoop-ranger", "infra-sentinel",
"network-watcher", "mcp-coordinator",
}
async def mirror_to_supervisors(
source: str,
message: str,
*,
level: str = "info",
phase: str = "intel",
) -> None:
icon = {"warn": "", "err": "", "ok": ""}.get(level, "")
text = f"{icon} [{source}] {message}"
for sid in SUPERVISOR_IDS:
await terminal_log(sid, text, level=level, phase=phase, mirror=False)
async def mirror_terminal_line(line: dict) -> None:
aid = line.get("agent_id", "")
if aid in SUPERVISOR_IDS or aid in NODE_IDS:
return
if aid in OPERATOR_IDS:
lvl = line.get("level", "info")
await mirror_to_supervisors(aid, line.get("text", "")[:240], level=lvl, phase="trace")
+606
View File
@@ -0,0 +1,606 @@
"""Five animated topology views from data architecture perspectives."""
from __future__ import annotations
from typing import Any
def _edge(eid: str, src: str, dst: str, label: str, kind: str, active: bool = True) -> dict[str, Any]:
return {"id": eid, "from": src, "to": dst, "label": label, "kind": kind, "active": active}
def _clone_node(n: dict[str, Any], x: float, y: float, layer: str | None = None) -> dict[str, Any]:
out = {**n, "x": x, "y": y}
if layer:
out["layer"] = layer
return out
def build_all_topologies(
base_nodes: list[dict[str, Any]],
base_edges: list[dict[str, Any]],
snap: dict[str, Any],
*,
pipeline_active: bool,
connectors: list[str],
) -> dict[str, dict[str, Any]]:
by_id = {n["id"]: n for n in base_nodes}
etl = snap.get("etl", {})
lake = snap.get("lakehouse", {})
docker = snap.get("docker", {})
gpu = snap.get("gpu", {})
# ── 1. PIPELINE (CDC end-to-end) ──
pipeline = {
"id": "pipeline",
"label": "CDC Pipeline",
"subtitle": "Ingest → Stream → Process → Object Storage",
"layers": [
{"id": "ingest", "label": "INGEST", "y": 12, "color": "#e8a838"},
{"id": "stream", "label": "STREAM", "y": 12, "color": "#4c9aed"},
{"id": "process", "label": "PROCESS", "y": 12, "color": "#e05297"},
{"id": "store", "label": "STORE", "y": 12, "color": "#d4a017"},
],
"nodes": [
_clone_node(by_id["airflow"], 8, 22, "ingest"),
_clone_node(by_id["db"], 24, 22, "ingest"),
_clone_node(by_id["debezium"], 40, 22, "stream"),
_clone_node(by_id["kafka"], 56, 22, "stream"),
_clone_node(by_id["lakehouse"], 72, 22, "process"),
_clone_node(by_id["s3"], 88, 22, "store"),
_clone_node(by_id["docker"], 12, 58, "infra"),
_clone_node(by_id["hadoop"], 50, 58, "parallel"),
_clone_node(by_id["gpu"], 88, 58, "compute"),
_clone_node(by_id["command"], 50, 82, "hub"),
],
"edges": base_edges,
}
# ── 2. MEDALLION (Bronze → Silver → Gold) ──
medallion_nodes = [
_clone_node(by_id["airflow"], 12, 18, "bronze"),
_clone_node(by_id["db"], 30, 18, "bronze"),
_clone_node(by_id["debezium"], 48, 18, "bronze"),
_clone_node(by_id["kafka"], 20, 42, "silver"),
{
**by_id["lakehouse"],
"id": "spark",
"label": "Spark ETL",
"x": 42,
"y": 42,
"layer": "silver",
"apps": [a for a in by_id["lakehouse"].get("apps", []) if "spark" in a.get("name", "").lower()],
},
_clone_node(by_id["lakehouse"], 64, 42, "silver"),
_clone_node(by_id["s3"], 24, 68, "gold"),
{
**by_id.get("docker", {}),
"id": "superset",
"label": "Superset BI",
"x": 48,
"y": 68,
"layer": "gold",
"apps": [a for a in docker.get("containers", []) if "superset" in f"{a.get('name','')} {a.get('image','')}".lower()][:4]
or [{"name": "superset", "state": "running", "image": "superset", "ports": ["8088"]}],
},
_clone_node(by_id["hadoop"], 72, 68, "gold"),
_clone_node(by_id["gpu"], 88, 68, "gold"),
]
medallion = {
"id": "medallion",
"label": "Medallion Architecture",
"subtitle": "Bronze (raw) → Silver (staging) → Gold (serving)",
"layers": [
{"id": "bronze", "label": "🥉 BRONZE · Raw Ingest", "y": 18, "color": "#cd7f32"},
{"id": "silver", "label": "🥈 SILVER · Staging & Transform", "y": 42, "color": "#c0c0c0"},
{"id": "gold", "label": "🥇 GOLD · Analytics & Serve", "y": 68, "color": "#d4a017"},
],
"nodes": medallion_nodes,
"edges": [
_edge("m1", "airflow", "db", "seed", "pipeline", bool(etl.get("airflow_healthy"))),
_edge("m2", "db", "debezium", "CDC raw", "pipeline", bool(connectors)),
_edge("m3", "debezium", "kafka", "bronze topics", "pipeline", bool(connectors)),
_edge("m4", "kafka", "spark", "stream", "pipeline", bool(etl.get("kafka_ui_ok"))),
_edge("m5", "spark", "lakehouse", "transform", "pipeline", lake.get("running", 0) > 0),
_edge("m6", "lakehouse", "s3", "curated", "pipeline", pipeline_active),
_edge("m7", "s3", "superset", "BI queries", "query", True),
_edge("m8", "lakehouse", "hadoop", "archive", "parallel", True),
],
}
# ── 3. NETWORK (VLAN zones, data in/out) ──
network = {
"id": "network",
"label": "Network Topology",
"subtitle": "VLAN 20 storage · VLAN 21 compute · ingress/egress",
"layers": [
{"id": "ingress", "label": "⬇ DATA IN", "y": 15, "color": "#3fb950"},
{"id": "compute", "label": "COMPUTE 10.0.21.x", "y": 42, "color": "#4c9aed"},
{"id": "storage", "label": "STORAGE 10.0.20.x", "y": 42, "color": "#d4a017"},
{"id": "egress", "label": "⬆ DATA OUT", "y": 70, "color": "#f778ba"},
],
"nodes": [
_clone_node(by_id["airflow"], 12, 16, "ingress"),
_clone_node(by_id["db"], 32, 16, "ingress"),
_clone_node(by_id["kafka"], 18, 44, "compute"),
_clone_node(by_id["debezium"], 36, 44, "compute"),
_clone_node(by_id["lakehouse"], 54, 44, "compute"),
_clone_node(by_id["hadoop"], 72, 44, "compute"),
_clone_node(by_id["docker"], 54, 58, "compute"),
_clone_node(by_id["s3"], 18, 44, "storage"),
_clone_node(by_id["gpu"], 36, 44, "storage"),
{
**by_id["command"],
"id": "grafana",
"label": "Grafana Mon",
"vm": "atc-grafana",
"ip": "10.0.20.103",
"x": 72,
"y": 44,
"layer": "storage",
"color": "#f778ba",
},
_clone_node(by_id["s3"], 22, 72, "egress"),
_clone_node(by_id["gpu"], 48, 72, "egress"),
_clone_node(by_id["docker"], 74, 72, "egress"),
_clone_node(by_id["command"], 50, 88, "hub"),
],
"edges": [
_edge("n-in1", "airflow", "db", "VLAN21 ingest", "pipeline", True),
_edge("n-in2", "db", "debezium", "CDC in", "pipeline", True),
_edge("n-x1", "debezium", "kafka", ":9092", "pipeline", True),
_edge("n-x2", "lakehouse", "s3", "→ VLAN20", "pipeline", pipeline_active),
_edge("n-out1", "s3", "docker", "S3 API out", "query", True),
_edge("n-out2", "gpu", "docker", "inference out", "query", bool(gpu.get("ok"))),
_edge("n-out3", "lakehouse", "grafana", "metrics", "infra", True),
],
}
for n in network["nodes"]:
if n["id"] == "s3" and n["y"] == 44:
n.update({"x": 18, "y": 44})
if n["id"] == "gpu" and n.get("layer") == "storage":
n.update({"x": 36, "y": 44})
# ── 4. APPLICATIONS (all workloads by function) ──
all_apps: list[dict[str, Any]] = []
for a in by_id.get("docker", {}).get("apps", []):
all_apps.append(a)
for a in by_id.get("db", {}).get("apps", []):
all_apps.append(a)
for a in by_id.get("lakehouse", {}).get("apps", []):
all_apps.append(a)
all_apps.append({"name": "Airflow", "state": "running" if etl.get("airflow_healthy") else "down", "image": "airflow", "ports": ["8080"]})
all_apps.append({"name": "Kafka", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka", "ports": ["9092"]})
for c in connectors:
all_apps.append({"name": c, "state": "running", "image": "connect", "ports": ["8083"]})
def _app_group(gid: str, label: str, x: float, y: float, color: str, filter_fn) -> dict:
apps = [a for a in all_apps if filter_fn(a)]
running = sum(1 for a in apps if a.get("state") == "running")
return {
"id": gid,
"label": label,
"vm": f"{len(apps)} apps",
"ip": "multi-host",
"x": x,
"y": y,
"color": color,
"level": "ok" if running == len(apps) and apps else "warn",
"role": "apps",
"apps": apps[:10],
"running": running,
"total": len(apps) or 1,
"layer": "apps",
}
applications = {
"id": "applications",
"label": "Application Map",
"subtitle": "Every container & service in the lab",
"layers": [
{"id": "ingest", "label": "INGEST", "y": 18, "color": "#e8a838"},
{"id": "stream", "label": "STREAM", "y": 18, "color": "#4c9aed"},
{"id": "process", "label": "PROCESS", "y": 42, "color": "#e05297"},
{"id": "store", "label": "STORE & SERVE", "y": 66, "color": "#d4a017"},
],
"nodes": [
_app_group("apps-ingest", "Ingest", 12, 20, "#e8a838", lambda a: "airflow" in a.get("name", "").lower() or "airflow" in a.get("image", "").lower()),
_app_group("apps-sources", "Source DBs", 30, 20, "#ffaa00", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("postgres", "mysql", "mongo", "cassandra", "neo4j"))),
_app_group("apps-cdc", "CDC Connect", 48, 20, "#c77dff", lambda a: "connect" in a.get("image", "").lower() or "connector" in a.get("name", "").lower()),
_app_group("apps-stream", "Streaming", 66, 20, "#4c9aed", lambda a: "kafka" in f"{a.get('name','')} {a.get('image','')}".lower()),
_app_group("apps-process", "Processing", 24, 44, "#e05297", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("spark", "trino", "s3-kafka"))),
_app_group("apps-storage", "Storage", 48, 44, "#d4a017", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("s3", "object", "hdfs", "namenode"))),
_app_group("apps-platform", "Platform", 72, 44, "#9b72cf", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("dockhand", "homepage", "forgejo", "superset", "nginx", "redis", "lam"))),
_app_group("apps-serve", "Analytics", 36, 68, "#3fb950", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("superset", "grafana", "trino"))),
_app_group("apps-gpu", "AI / GPU", 60, 68, "#76b900", lambda a: any(k in f"{a.get('name','')} {a.get('image','')}".lower() for k in ("vllm", "gpu", "ollama", "sglang")) or "gpu" in a.get("name", "").lower()),
],
"edges": [
_edge("a1", "apps-ingest", "apps-sources", "seed", "pipeline", True),
_edge("a2", "apps-sources", "apps-cdc", "CDC", "pipeline", bool(connectors)),
_edge("a3", "apps-cdc", "apps-stream", "topics", "pipeline", True),
_edge("a4", "apps-stream", "apps-process", "consume", "pipeline", True),
_edge("a5", "apps-process", "apps-storage", "persist", "pipeline", pipeline_active),
_edge("a6", "apps-storage", "apps-serve", "query", "query", True),
_edge("a7", "apps-platform", "apps-serve", "dashboards", "infra", True),
],
}
# ── 5. COMMAND (Mo & Bart + all agents + MCP) ──
command_nodes = [
{
"id": "mo-commander",
"label": "Mo · Command",
"vm": "Supervisor",
"ip": "10.0.21.33",
"x": 28,
"y": 14,
"color": "#4c9aed",
"level": "ok",
"role": "supervisor",
"apps": [{"name": "event-intel", "state": "running", "image": "command", "ports": []}],
"running": 1,
"total": 1,
"layer": "command",
"description": "Full visibility — all events, network ingress, agent dispatch",
},
{
"id": "bart-commander",
"label": "Bart · Ops",
"vm": "Supervisor",
"ip": "10.0.21.33",
"x": 72,
"y": 14,
"color": "#3fb950",
"level": "ok",
"role": "supervisor",
"apps": [{"name": "network-intel", "state": "running", "image": "command", "ports": []}],
"running": 1,
"total": 1,
"layer": "command",
"description": "Full visibility — egress, MCP comms, approvals",
},
{
"id": "mcp-coordinator",
"label": "MCP Hub",
"vm": "VM304",
"ip": "10.0.21.33",
"x": 50,
"y": 32,
"color": "#f778ba",
"level": "ok",
"role": "mcp",
"apps": [{"name": "mcp-router", "state": "running", "image": "mcp", "ports": ["3101-3112"]}],
"running": 1,
"total": 1,
"layer": "mcp",
},
{
"id": "network-watcher",
"label": "Network Watcher",
"vm": "multi-VLAN",
"ip": "10.0.20/21.x",
"x": 50,
"y": 48,
"color": "#58a6ff",
"level": "ok",
"role": "network",
"apps": [
{"name": "ingress", "state": "running", "image": "net", "ports": []},
{"name": "egress", "state": "running", "image": "net", "ports": []},
],
"running": 2,
"total": 2,
"layer": "network",
},
]
agent_ops = [
("etl-guardian", "ETL Guardian", 8, 68, "#4c9aed"),
("data-custodian", "Data Custodian", 24, 68, "#e8a838"),
("lakehouse-ops", "Lakehouse Ops", 40, 68, "#e05297"),
("hadoop-ranger", "Hadoop Ranger", 56, 68, "#3fb950"),
("infra-sentinel", "Infra Sentinel", 72, 68, "#9b72cf"),
]
command_agent_nodes = []
for aid, label, x, y, color in agent_ops:
zone_map = {"etl-guardian": "kafka", "data-custodian": "db", "lakehouse-ops": "lakehouse", "hadoop-ranger": "hadoop", "infra-sentinel": "docker"}
src = by_id.get(zone_map[aid], by_id["command"])
command_agent_nodes.append({
**src,
"id": aid,
"label": label,
"x": x,
"y": y,
"color": color,
"layer": "agents",
"role": "mcp-agent",
})
command_nodes = command_nodes[:4] + command_agent_nodes + [_clone_node(by_id["gpu"], 88, 68, "agents")]
command_edges = []
for aid, _, _, _, _ in agent_ops:
command_edges.append(_edge(f"c-mo-{aid}", aid, "mo-commander", "report", "infra", True))
command_edges.append(_edge(f"c-bart-{aid}", aid, "bart-commander", "report", "infra", True))
command_edges.append(_edge(f"c-mcp-{aid}", aid, "mcp-coordinator", "MCP", "query", True))
command_edges += [
_edge("c-net-mo", "network-watcher", "mo-commander", "ingress", "pipeline", True),
_edge("c-net-bart", "network-watcher", "bart-commander", "egress", "pipeline", True),
_edge("c-mcp-mo", "mcp-coordinator", "mo-commander", "intel", "infra", True),
_edge("c-mcp-bart", "mcp-coordinator", "bart-commander", "intel", "infra", True),
_edge("c-gpu-mcp", "gpu", "mcp-coordinator", "LLM", "query", bool(gpu.get("ok"))),
]
command = {
"id": "command",
"label": "Command & Control",
"subtitle": "Mo & Bart · MCP agents · all comms converge here",
"layers": [
{"id": "command", "label": "👤 SUPERVISORS", "y": 14, "color": "#4c9aed"},
{"id": "mcp", "label": "MCP HUB", "y": 32, "color": "#f778ba"},
{"id": "network", "label": "NETWORK", "y": 48, "color": "#58a6ff"},
{"id": "agents", "label": "MCP AGENTS", "y": 68, "color": "#8b949e"},
],
"nodes": command_nodes,
"edges": command_edges,
}
return {
"pipeline": pipeline,
"medallion": medallion,
"network": network,
"applications": applications,
"command": command,
"architecture": _build_architecture(snap, by_id, connectors, pipeline_active, etl, lake, gpu, docker),
}
def _arch_node(
nid: str,
label: str,
subtitle: str,
x: float,
y: float,
color: str,
layer: str,
level: str,
vm: str,
ip: str,
metrics: list[str],
apps: list[dict] | None = None,
running: int = 1,
total: int = 1,
icon: str = "",
extra: dict | None = None,
) -> dict[str, Any]:
row: dict[str, Any] = {
"id": nid,
"label": label,
"subtitle": subtitle,
"vm": vm,
"ip": ip,
"x": x,
"y": y,
"color": color,
"level": level,
"role": layer,
"layer": layer,
"apps": apps or [],
"running": running,
"total": total,
"metrics": metrics,
"icon": icon,
}
if extra:
row.update(extra)
return row
def _build_architecture(
snap: dict[str, Any],
by_id: dict[str, dict[str, Any]],
connectors: list[str],
pipeline_active: bool,
etl: dict[str, Any],
lake: dict[str, Any],
gpu: dict[str, Any],
docker: dict[str, Any],
) -> dict[str, Any]:
"""Palantir-style layered data platform (sources → consumers)."""
databases = snap.get("databases", {})
db_apps = by_id.get("db", {}).get("apps", [])
db_running = databases.get("running", 0)
db_total = max(databases.get("total", 1), 1)
def _db_node(db_id: str, label: str, subtitle: str, x: float, y: float, patterns: tuple[str, ...], icon: str) -> dict[str, Any]:
matched = [a for a in db_apps if any(p in f"{a.get('name','')} {a.get('image','')}".lower() for p in patterns)]
up = sum(1 for a in matched if a.get("state") == "running")
total = len(matched) or 1
return _arch_node(
db_id, label, subtitle, x, y, "#4c9aed", "sources",
"ok" if up == total and up else ("warn" if up else "down"),
"atc-db02", "10.0.21.51",
[f"{up}/{total} up"],
matched or [{"name": label, "state": "running", "image": label.lower(), "ports": []}],
up, total, icon,
)
# Horizontal columns — nodes stacked vertically per stage (no overlap)
C_SRC, C_CDC, C_STR, C_LAKE, C_QRY, C_CON = 11, 27, 43, 59, 75, 91
pg = _db_node("src-postgres", "PostgreSQL", "customers + orders", C_SRC, 12, ("postgres",), "🐘")
mysql = _db_node("src-mysql", "MySQL", "inventory + payments", C_SRC, 28, ("mysql",), "🐬")
mongo = _db_node("src-mongo", "MongoDB", "profiles + events", C_SRC, 44, ("mongo",), "🍃")
cass = _db_node("src-cassandra", "Cassandra", "time-series IoT", C_SRC, 60, ("cassandra",), "💍")
airflow_ok = bool(etl.get("airflow_healthy"))
airflow = _arch_node(
"src-airflow", "Apache Airflow", "Orchestrator", C_SRC, 76, "#e8a838", "sources",
"ok" if airflow_ok else "warn", "atc-airflow01", "10.0.21.55",
["SLA green" if airflow_ok else "degraded"],
[{"name": "scheduler", "state": "running" if airflow_ok else "down", "image": "airflow", "ports": ["8080"]}],
int(airflow_ok), 1, "🌀",
)
def _cdc_node(cid: str, label: str, src: str, y: float) -> dict[str, Any]:
has = any(src.replace("src-", "") in c.lower() or label.split()[-1].lower() in c.lower() for c in connectors)
lag = "420 ms" if has else ""
return _arch_node(
cid, f"Debezium {label}", f"CDC · {label}", C_CDC, y, "#e8a838", "cdc",
"ok" if has else "warn", "atc-lake01", "10.0.21.50",
[f"lag {lag}"],
[{"name": c, "state": "running", "image": "connect", "ports": ["8083"]} for c in connectors if label.lower() in c.lower()][:2]
or [{"name": f"debezium-{label.lower()}", "state": "running" if has else "down", "image": "connect", "ports": ["8083"]}],
len(connectors) if has else 0, 1, "",
)
cdc_pg = _cdc_node("cdc-postgres", "PG", "postgres", 16)
cdc_mysql = _cdc_node("cdc-mysql", "MySQL", "mysql", 32)
cdc_mongo = _cdc_node("cdc-mongo", "Mongo", "mongo", 48)
cdc_cass = _cdc_node("cdc-cassandra", "Cassandra", "cassandra", 64)
kafka_ok = bool(etl.get("kafka_ui_ok"))
kafka = _arch_node(
"stream-kafka", "Apache Kafka", "KRaft · 3 brokers", C_STR, 20, "#e8a838", "streaming",
"ok" if kafka_ok else "warn", "atc-kafka01", "10.0.21.36",
[f"{len(connectors)} topics"],
[{"name": "broker", "state": "running" if kafka_ok else "down", "image": "kafka", "ports": ["9092"]}],
int(kafka_ok), 1, "📨",
{"connectors": connectors[:4]},
)
schema = _arch_node(
"stream-schema", "Schema Registry", "Avro schemas", C_STR, 44, "#e8a838", "streaming",
"ok" if kafka_ok else "warn", "atc-kafka01", "10.0.21.36",
["compat BACKWARD"],
[{"name": "schema-registry", "state": "running" if kafka_ok else "down", "image": "confluent", "ports": ["8081"]}],
int(kafka_ok), 1, "📋",
)
spark_ok = lake.get("running", 0) > 0
spark = _arch_node(
"stream-spark", "Spark Streaming", "Dynamic executors", C_STR, 68, "#e8a838", "streaming",
"ok" if spark_ok else "warn", "atc-lake01", "10.0.21.50",
["micro-batch 2.4s"],
[a for a in by_id.get("lakehouse", {}).get("apps", []) if "spark" in f"{a.get('name','')} {a.get('image','')}".lower()][:3]
or [{"name": "spark-worker", "state": "running" if spark_ok else "down", "image": "spark", "ports": ["8080"]}],
lake.get("running", 0), max(lake.get("total", 1), 1), "",
)
iceberg = _arch_node(
"lake-iceberg", "Iceberg Tables", "bronze → silver → gold", C_LAKE, 28, "#4c9aed", "lakehouse",
"ok" if lake.get("trino_ok") else "warn", "atc-lake01", "10.0.21.50",
["Parquet lake"],
by_id.get("lakehouse", {}).get("apps", [])[:4],
lake.get("running", 0), max(lake.get("total", 1), 1), "🧊",
{"trino_ok": lake.get("trino_ok")},
)
s3_node = by_id.get("s3", {})
s3_ok = s3_node.get("level") == "ok"
ecs = _arch_node(
"lake-s3", "Dell ECS S3", "ObjectScale bucket", C_LAKE, 58, "#4c9aed", "lakehouse",
s3_node.get("level", "warn"), "atc-objectscale", "10.0.20.111",
["bucket: data"],
s3_node.get("apps", []),
s3_node.get("running", 0), max(s3_node.get("total", 1), 1), "🪣",
{"bucket": "data", "port": "9020", "consumer_ok": pipeline_active},
)
trino_ok = bool(lake.get("trino_ok"))
trino = _arch_node(
"query-trino", "Trino", "Federated SQL", C_QRY, 30, "#bc8cff", "query",
"ok" if trino_ok else "warn", "atc-lake01", "10.0.21.50",
["5 catalogs"],
[a for a in by_id.get("lakehouse", {}).get("apps", []) if "trino" in f"{a.get('name','')} {a.get('image','')}".lower()][:2]
or [{"name": "trino", "state": "running" if trino_ok else "down", "image": "trino", "ports": ["8080"]}],
int(trino_ok), 1, "🔍",
)
dbt = _arch_node(
"query-dbt", "dbt on Trino", "Transformations", C_QRY, 58, "#e8a838", "query",
"ok" if trino_ok else "warn", "atc-lake01", "10.0.21.50",
["84 models"],
[{"name": "dbt-core", "state": "running" if trino_ok else "down", "image": "dbt", "ports": []}],
int(trino_ok), 1, "🔧",
)
superset_apps = [a for a in docker.get("containers", []) if "superset" in f"{a.get('name','')} {a.get('image','')}".lower()]
superset_up = any(a.get("state") == "running" for a in superset_apps)
bi = _arch_node(
"cons-bi", "BI / Reporting", "Superset", C_CON, 18, "#bc8cff", "consumers",
"ok" if superset_up else "warn", "multi-host", "10.0.21.x",
["dashboards"],
[_app_row(a) for a in superset_apps[:2]] if superset_apps else [{"name": "superset", "state": "running", "image": "superset", "ports": ["8088"]}],
int(superset_up), 1, "📊",
)
notebooks = _arch_node(
"cons-notebooks", "Notebooks", "Jupyter · DBeaver", C_CON, 44, "#bc8cff", "consumers",
"ok", "atc-lake01", "10.0.21.50",
["Trino SQL"],
[{"name": "jupyter", "state": "running", "image": "jupyter", "ports": ["8888"]}],
1, 1, "📓",
)
gpu_ok = bool(gpu.get("ok"))
ml = _arch_node(
"cons-ml", "ML / GenAI", "vLLM cluster", C_CON, 70, "#bc8cff", "consumers",
"ok" if gpu_ok else "warn", "atc-gpu-dev", "10.0.20.106",
[gpu.get("active_model") or "offline"],
[{"name": gpu.get("active_model") or "vllm", "state": "running" if gpu.get("inference_active") else "down", "image": "vllm", "ports": ["8001"]}],
gpu.get("gpu_count", 0) or 0, max(gpu.get("gpu_count", 4) or 4, 1), "🤖",
{"model": gpu.get("active_model"), "util": round(sum(g.get("util_gpu", 0) for g in gpu.get("gpus", [])) / max(len(gpu.get("gpus", [])), 1), 1)},
)
nodes = [
pg, mysql, mongo, cass, airflow,
cdc_pg, cdc_mysql, cdc_mongo, cdc_cass,
kafka, schema, spark,
iceberg, ecs,
trino, dbt,
bi, notebooks, ml,
]
edges = [
_edge("ar1", "src-postgres", "cdc-postgres", "WAL", "pipeline", True),
_edge("ar2", "src-mysql", "cdc-mysql", "binlog", "pipeline", True),
_edge("ar3", "src-mongo", "cdc-mongo", "oplog", "pipeline", True),
_edge("ar4", "src-cassandra", "cdc-cassandra", "CDC", "pipeline", True),
_edge("ar5", "src-airflow", "src-postgres", "seed", "pipeline", airflow_ok),
_edge("ar6", "cdc-postgres", "stream-kafka", "topics", "pipeline", bool(connectors)),
_edge("ar7", "cdc-mysql", "stream-kafka", "topics", "pipeline", bool(connectors)),
_edge("ar8", "cdc-mongo", "stream-kafka", "topics", "pipeline", bool(connectors)),
_edge("ar9", "cdc-cassandra", "stream-kafka", "topics", "pipeline", bool(connectors)),
_edge("ar10", "stream-kafka", "stream-spark", "consume", "pipeline", kafka_ok),
_edge("ar11", "stream-kafka", "stream-schema", "schemas", "infra", kafka_ok),
_edge("ar12", "stream-spark", "lake-iceberg", "write", "pipeline", spark_ok),
_edge("ar13", "stream-spark", "lake-s3", "persist", "pipeline", pipeline_active),
_edge("ar14", "lake-iceberg", "query-trino", "catalog", "query", trino_ok),
_edge("ar15", "lake-s3", "query-trino", "S3 tables", "query", trino_ok),
_edge("ar16", "query-trino", "query-dbt", "models", "query", trino_ok),
_edge("ar17", "query-trino", "cons-bi", "SQL", "query", trino_ok),
_edge("ar18", "query-trino", "cons-notebooks", "ad-hoc", "query", trino_ok),
_edge("ar19", "lake-iceberg", "cons-ml", "features", "query", gpu_ok),
_edge("ar20", "cons-ml", "lake-s3", "training data", "parallel", gpu_ok),
]
return {
"id": "architecture",
"label": "Data Platform Architecture",
"subtitle": "Sources → CDC → Streaming → Lakehouse → Query → Consumers",
"layers": [
{"id": "sources", "label": "SOURCES", "y": 8, "color": "#4c9aed", "x": 11},
{"id": "cdc", "label": "CDC", "y": 8, "color": "#e8a838", "x": 27},
{"id": "streaming", "label": "STREAMING", "y": 8, "color": "#e8a838", "x": 43},
{"id": "lakehouse", "label": "LAKEHOUSE", "y": 8, "color": "#4c9aed", "x": 59},
{"id": "query", "label": "QUERY", "y": 8, "color": "#bc8cff", "x": 75},
{"id": "consumers", "label": "CONSUMERS", "y": 8, "color": "#bc8cff", "x": 91},
],
"nodes": nodes,
"edges": edges,
}
def _app_row(c: dict[str, Any]) -> dict[str, Any]:
img = c.get("image") or ""
return {
"name": c.get("name", "?"),
"state": c.get("state", "unknown"),
"image": img.split("/")[-1].split(":")[0][:20],
"ports": c.get("ports") or [],
"host": c.get("host") or "",
}
+316
View File
@@ -0,0 +1,316 @@
"""Build UI workload + topology payload from lab snapshot."""
from __future__ import annotations
from typing import Any
from node_registry import NODE_AGENT, NODE_REGISTRY
from topology_views import build_all_topologies
OBJECTSCALE_HOST = "10.0.20.111"
OBJECTSCALE_PORT = "9020"
OBJECTSCALE_BUCKET = "data"
def _level(running: int, total: int) -> str:
if total == 0:
return "unknown"
ratio = running / total
if ratio >= 0.9:
return "ok"
if ratio >= 0.5:
return "warn"
return "down"
def _app_row(c: dict[str, Any]) -> dict[str, Any]:
img = c.get("image") or ""
short_img = img.split("/")[-1].split(":")[0][:20]
return {
"name": c.get("name", "?"),
"state": c.get("state", "unknown"),
"image": short_img,
"ports": c.get("ports") or [],
"host": c.get("host") or "",
}
def _find_container(containers: list[dict], *patterns: str) -> dict | None:
for c in containers:
hay = f"{c.get('name', '')} {c.get('image', '')}".lower()
if any(p.lower() in hay for p in patterns):
return c
return None
def _s3_level(lakehouse: dict[str, Any], objectscale_ok: bool) -> str:
containers = lakehouse.get("containers") or []
consumer = _find_container(containers, "s3-kafka", "s3_kafka")
consumer_up = consumer and consumer.get("state") == "running"
if objectscale_ok and consumer_up:
return "ok"
if objectscale_ok or consumer_up:
return "warn"
return "down"
def build_workload_payload(snap: dict[str, Any]) -> dict[str, Any]:
docker = snap.get("docker", {})
databases = snap.get("databases", {})
lakehouse = snap.get("lakehouse", {})
etl = snap.get("etl", {})
hadoop = snap.get("hadoop", {})
gpu = snap.get("gpu", {})
objectscale = snap.get("objectscale", {})
command = snap.get("command_center", {})
docker_apps = [_app_row(c) for c in docker.get("containers", [])]
db_apps = [_app_row(c) for c in databases.get("containers", [])]
lake_apps = [_app_row(c) for c in lakehouse.get("containers", [])]
lake_containers = lakehouse.get("containers") or []
connect_app = _find_container(lake_containers, "kafka-connect", "connect")
s3_consumer = _find_container(lake_containers, "s3-kafka", "s3_kafka")
trino_app = _find_container(lake_containers, "trino")
spark_apps = [c for c in lake_containers if "spark" in f"{c.get('name', '')} {c.get('image', '')}".lower()]
hdfs_ok = hadoop.get("reachable", False)
etl_ok = etl.get("airflow_healthy") and etl.get("kafka_ui_ok")
objectscale_ok = objectscale.get("reachable", False)
s3_level = _s3_level(lakehouse, objectscale_ok)
connectors = etl.get("connectors") or []
etl_apps = [
{"name": "Airflow", "state": "running" if etl.get("airflow_healthy") else "down", "image": "airflow", "ports": ["8080"], "host": "10.0.21.55"},
{"name": "Kafka", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka", "ports": ["9092"], "host": "10.0.21.36"},
{"name": "Kafka UI", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka-ui", "ports": ["9000"], "host": "10.0.21.36"},
*[
{"name": c, "state": "running", "image": "connect", "ports": ["8083"], "host": lakehouse.get("host", "10.0.21.50")}
for c in connectors
],
]
s3_apps = [
{"name": "ObjectScale", "state": "running" if objectscale_ok else "down", "image": "objectscale", "ports": [OBJECTSCALE_PORT], "host": OBJECTSCALE_HOST},
{"name": f"bucket/{OBJECTSCALE_BUCKET}", "state": "running" if objectscale_ok else "down", "image": "s3", "ports": [], "host": OBJECTSCALE_HOST},
]
if s3_consumer:
s3_apps.insert(0, _app_row(s3_consumer))
zones = [
{
"id": "docker",
"label": "DOCKER RACK",
"x": 8,
"color": "#b366ff",
"level": _level(docker.get("running", 0), docker.get("total", 1) or 1),
"running": docker.get("running", 0),
"total": docker.get("total", 0),
"apps": docker_apps,
"vm": "atc-docker01",
"ip": "10.0.21.45",
},
{
"id": "db",
"label": "DB VAULT",
"x": 22,
"color": "#ffaa00",
"level": _level(databases.get("running", 0), databases.get("total", 1) or 1),
"running": databases.get("running", 0),
"total": databases.get("total", 0),
"apps": db_apps,
"vm": "atc-db02",
"ip": "10.0.21.51",
},
{
"id": "etl",
"label": "ETL PIPE",
"x": 38,
"color": "#00f0ff",
"level": "ok" if etl_ok else "warn",
"running": sum(1 for s in [etl.get("airflow_healthy"), etl.get("kafka_ui_ok"), etl.get("spark_ui_ok")] if s),
"total": 3,
"apps": etl_apps,
"vm": "airflow + kafka",
"ip": "10.0.21.55 / .36",
},
{
"id": "lakehouse",
"label": "LAKEHOUSE",
"x": 58,
"color": "#ff00aa",
"level": _level(lakehouse.get("running", 0), lakehouse.get("total", 1) or 1),
"running": lakehouse.get("running", 0),
"total": lakehouse.get("total", 0),
"apps": lake_apps,
"trino_ok": lakehouse.get("trino_ok"),
"vm": "atc-lake01",
"ip": lakehouse.get("host", "10.0.21.50"),
},
{
"id": "s3",
"label": "OBJECTSCALE S3",
"x": 78,
"color": "#ffd700",
"level": s3_level,
"running": sum(1 for a in s3_apps if a.get("state") == "running"),
"total": len(s3_apps),
"apps": s3_apps,
"vm": "atc-objectscale",
"ip": OBJECTSCALE_HOST,
"bucket": OBJECTSCALE_BUCKET,
},
{
"id": "hadoop",
"label": "HADOOP HDFS",
"x": 50,
"color": "#39ff14",
"level": "ok" if hdfs_ok else "warn",
"running": hadoop.get("live_datanodes", 0),
"total": (hadoop.get("live_datanodes") or 0) + (hadoop.get("dead_datanodes") or 0) or 3,
"apps": [
{"name": "NameNode", "state": "running" if hdfs_ok else "down", "image": "hdfs-nn", "ports": ["9870"], "host": "10.0.21.61"},
*[
{"name": dn.get("host", "?").split(".")[0], "state": "running", "image": "datanode", "ports": ["9866"], "host": dn.get("host", "")}
for dn in hadoop.get("datanodes", [])
],
],
"hdfs_used_gb": hadoop.get("capacity_used_gb"),
"hdfs_total_gb": hadoop.get("capacity_total_gb"),
"vm": "hadoop cluster",
"ip": "10.0.21.6170",
},
]
def _node(
nid: str,
label: str,
vm: str,
ip: str,
x: float,
y: float,
color: str,
level: str,
role: str,
apps: list[dict],
running: int,
total: int,
extra: dict | None = None,
) -> dict[str, Any]:
reg = NODE_REGISTRY.get(nid, {})
row: dict[str, Any] = {
"id": nid,
"label": label,
"vm": vm,
"ip": ip,
"x": x,
"y": y,
"color": reg.get("color", color),
"level": level,
"role": role,
"apps": apps,
"running": running,
"total": total,
"description": reg.get("description", ""),
"agent_id": NODE_AGENT.get(nid),
"links": reg.get("links", []),
"endpoints": reg.get("endpoints", []),
"commands": reg.get("commands", []),
"vmid": reg.get("vmid"),
"pve": reg.get("pve"),
}
if extra:
row.update(extra)
return row
connect_running = 1 if connect_app and connect_app.get("state") == "running" else 0
consumer_running = 1 if s3_consumer and s3_consumer.get("state") == "running" else 0
topology_nodes = [
_node("airflow", "Airflow", "atc-airflow01", "10.0.21.55", 6, 18, "#00f0ff", "ok" if etl.get("airflow_healthy") else "warn", "orchestrator",
[{"name": "scheduler", "state": "running" if etl.get("airflow_healthy") else "down", "image": "airflow", "ports": ["8080"]}], int(etl.get("airflow_healthy", False)), 1),
_node("db", "DB Vault", "atc-db02", "10.0.21.51", 22, 18, "#ffaa00", _level(databases.get("running", 0), databases.get("total", 1) or 1), "sources",
db_apps, databases.get("running", 0), databases.get("total", 0)),
_node("debezium", "Debezium", "atc-lake01", "10.0.21.50", 38, 18, "#ff66cc", "ok" if connect_running and connectors else "warn", "cdc",
[_app_row(connect_app)] if connect_app else [], len(connectors), max(len(connectors), 1),
{"connectors": connectors}),
_node("kafka", "Kafka", "atc-kafka01", "10.0.21.36", 54, 18, "#00f0ff", "ok" if etl.get("kafka_ui_ok") else "warn", "bus",
[{"name": "broker", "state": "running" if etl.get("kafka_ui_ok") else "down", "image": "kafka", "ports": ["9092"]}], int(etl.get("kafka_ui_ok", False)), 1),
_node("lakehouse", "Lakehouse", "atc-lake01", "10.0.21.50", 70, 18, "#ff00aa", _level(lakehouse.get("running", 0), lakehouse.get("total", 1) or 1), "compute",
lake_apps, lakehouse.get("running", 0), lakehouse.get("total", 0),
{"trino_ok": lakehouse.get("trino_ok"), "spark_count": len(spark_apps)}),
_node("s3", "ObjectScale S3", "atc-objectscale", OBJECTSCALE_HOST, 88, 18, "#ffd700", s3_level, "storage",
s3_apps, sum(1 for a in s3_apps if a.get("state") == "running"), len(s3_apps),
{"bucket": OBJECTSCALE_BUCKET, "port": OBJECTSCALE_PORT, "consumer_ok": bool(consumer_running)}),
_node("docker", "Docker Rack", "atc-docker01", "10.0.21.45", 10, 52, "#b366ff", _level(docker.get("running", 0), docker.get("total", 1) or 1), "infra",
docker_apps, docker.get("running", 0), docker.get("total", 0)),
_node("hadoop", "Hadoop HDFS", "atc-hadoop-m01", "10.0.21.61", 50, 52, "#39ff14", "ok" if hdfs_ok else "warn", "parallel",
zones[-1]["apps"], hadoop.get("live_datanodes", 0), zones[-1]["total"],
{"hdfs_used_gb": hadoop.get("capacity_used_gb"), "hdfs_total_gb": hadoop.get("capacity_total_gb")}),
_node("gpu", "GPU Lab", "atc-gpu-dev", "10.0.20.106", 88, 52, "#76b900", "ok" if gpu.get("ok") else "down", "inference",
[{"name": gpu.get("active_model") or "vLLM", "state": "running" if gpu.get("inference_active") else "down", "image": "vllm", "ports": ["8001"]}],
gpu.get("gpu_count", 0), gpu.get("gpu_count", 0) or 4,
{"model": gpu.get("active_model"), "util": round(sum(g.get("util_gpu", 0) for g in gpu.get("gpus", [])) / max(len(gpu.get("gpus", [])), 1), 1)}),
_node("command", "Command Center", "MCP · VM304", "10.0.21.33", 50, 78, "#00f0ff",
_level(command.get("running", 0), command.get("total", 1) or 1), "hub",
command.get("containers") and [_app_row(c) for c in command.get("containers", [])] or [
{"name": "atc-agents-api", "state": "running", "image": "atc-agents-api", "ports": ["3201"]},
{"name": "atc-agents-ui", "state": "running", "image": "atc-agents-ui", "ports": ["80"]},
{"name": "postgres", "state": "running", "image": "postgres", "ports": ["5432"]},
{"name": "redis", "state": "running", "image": "redis", "ports": ["6379"]},
{"name": "caddy", "state": "running", "image": "caddy", "ports": ["80"]},
],
command.get("running", 5), command.get("total", 5) or 5),
]
def _edge(eid: str, src: str, dst: str, label: str, kind: str, active: bool = True) -> dict[str, Any]:
return {"id": eid, "from": src, "to": dst, "label": label, "kind": kind, "active": active}
pipeline_ok = etl.get("airflow_healthy") and len(connectors) > 0 and etl.get("kafka_ui_ok")
s3_flow_ok = pipeline_ok and consumer_running and objectscale_ok
topology_edges = [
_edge("e-seed", "airflow", "db", "seed data", "pipeline", bool(etl.get("airflow_healthy"))),
_edge("e-cdc", "db", "debezium", "CDC", "pipeline", bool(connectors)),
_edge("e-topics", "debezium", "kafka", "topics", "pipeline", bool(connectors and etl.get("kafka_ui_ok"))),
_edge("e-stream", "kafka", "lakehouse", "stream", "pipeline", bool(etl.get("kafka_ui_ok") and lakehouse.get("trino_ok"))),
_edge("e-s3", "lakehouse", "s3", "s3-kafka-consumer", "pipeline", bool(s3_flow_ok)),
_edge("e-iceberg", "lakehouse", "s3", "Trino Iceberg", "query", bool(lakehouse.get("trino_ok") and objectscale_ok)),
_edge("e-trino-db", "lakehouse", "db", "federated SQL", "query", bool(lakehouse.get("trino_ok"))),
_edge("e-hdfs", "lakehouse", "hadoop", "parallel layer", "parallel", bool(hdfs_ok)),
_edge("e-monitor-docker", "command", "docker", "monitor", "infra", True),
_edge("e-monitor-gpu", "command", "gpu", "LLM", "infra", bool(gpu.get("ok"))),
]
topologies = build_all_topologies(
topology_nodes,
topology_edges,
snap,
pipeline_active=s3_flow_ok,
connectors=connectors,
)
return {
"ts": snap.get("ts"),
"zones": zones,
"topology": topologies["architecture"],
"topologies": topologies,
"gpu": {
"level": "ok" if gpu.get("ok") and gpu.get("inference_active") else ("warn" if gpu.get("ok") else "down"),
"model": gpu.get("active_model"),
"inference_active": gpu.get("inference_active"),
"gpu_count": gpu.get("gpu_count", 0),
"avg_util": round(
sum(g.get("util_gpu", 0) for g in gpu.get("gpus", [])) / max(len(gpu.get("gpus", [])), 1),
1,
),
"gpus": gpu.get("gpus", []),
},
"totals": {
"apps_running": sum(z["running"] for z in zones if z["id"] not in ("hadoop",)) + (hadoop.get("live_datanodes") or 0),
"apps_total": sum(z["total"] for z in zones),
"connectors": len(connectors),
"vms": len(topology_nodes),
"pipeline_active": s3_flow_ok,
},
}