SalHargis's picture
Update app.py
29ccc00 verified
import gradio as gr
import networkx as nx
import matplotlib.pyplot as plt
import pandas as pd
import random
import copy
import json
import io
# --- Local Imports (Must exist in your file system) ---
import config
from utils import calculate_metric
import metric_visualizations
# --- Constants ---
# Metrics description for tooltips/help
METRIC_DESCRIPTIONS = {
"Density": "Ratio of actual connections to potential connections.",
"Cyclomatic Number": "Number of fundamental independent loops.",
"Global Efficiency": "Measure (0-1) of information flow ease.",
"Supportive Gain": "Efficiency provided specifically by 'Soft' edges.",
"Brittleness Ratio": "Balance of Supportive (Soft) vs Essential (Hard) edges.",
"Critical Vulnerability": "Checks if 'Hard' skeleton is connected.",
"Interdependence": "% of edges crossing between different agents.",
"Total Cycles": "Total count of feedback loops.",
"Modularity": "How well the system divides into isolated groups.",
"Functional Redundancy": "Avg number of agents per function.",
"Collaboration Ratio": "% of functions with shared authority."
}
# --- State Management Class ---
class GraphState:
"""
Acts as the 'self' from your Tkinter app.
Holds the graph, history, and configuration for the current session.
"""
def __init__(self):
self.G = nx.DiGraph()
self.pos = {} # Store display positions
self.agents = config.DEFAULT_AGENTS.copy()
self.undo_stack = []
self.redo_stack = []
self.saved_snapshots = {} # For comparison
self.counter = 0 # Unique ID counter
def save_to_history(self):
"""Push current state to undo stack"""
# Deep copy the graph to ensure isolation
self.undo_stack.append(self.G.copy())
if len(self.undo_stack) > 10:
self.undo_stack.pop(0)
self.redo_stack.clear()
def get_node_choices(self):
"""Returns list of (Label, ID) tuples for Dropdowns"""
return [(d.get('label', str(n)), n) for n, d in self.G.nodes(data=True)]
# --- Global Initialization ---
# In Gradio, we instantiate this once. For multi-user isolation,
# you would pass this state through the function arguments,
# but for a Space demo, a global instance often suffices for simplicity
# unless you expect heavy concurrent traffic.
session = GraphState()
# --- Core Logic Functions ---
def render_plot(vis_mode="None", edge_filter="ALL"):
"""
Replaces the Canvas drawing logic.
Uses Matplotlib to render the JSAT layers and NetworkX graph.
"""
plt.figure(figsize=(12, 8))
ax = plt.gca()
# 1. Draw Layer Backgrounds
# We map config layers to Y-axis. Matplotlib Y increases upwards.
for name, y_val in config.JSAT_LAYERS.items():
plt.axhline(y=y_val, color='#e0e0e0', linestyle='--', zorder=0)
plt.text(50, y_val + 10, name, color='grey', fontsize=10, fontweight='bold', zorder=0)
if session.G.number_of_nodes() == 0:
plt.text(0.5, 0.5, "Graph is Empty.\nUse 'Editor' tab to add nodes.",
ha='center', va='center', transform=ax.transAxes, color='grey')
plt.axis('off')
return plt.gcf()
# 2. Filter Edges
edges_to_draw = []
for u, v, d in session.G.edges(data=True):
etype = d.get('type', config.EDGE_TYPE_HARD)
if edge_filter == "ALL" or etype == edge_filter:
edges_to_draw.append((u, v))
# 3. Draw Highlights (Visual Analytics)
# This replaces the yellow overlay logic
highlight_nodes = []
highlight_edges = []
if vis_mode == "Cycles":
hl_data = metric_visualizations.get_cycle_highlights(session.G)
for item in hl_data:
highlight_nodes.extend(item.get('nodes', []))
highlight_edges.extend(item.get('edges', []))
elif vis_mode == "Interdependence":
hl_data = metric_visualizations.get_interdependence_highlights(session.G)
for item in hl_data:
highlight_nodes.extend(item.get('nodes', []))
highlight_edges.extend(item.get('edges', []))
elif vis_mode == "Modularity":
hl_data = metric_visualizations.get_modularity_highlights(session.G)
for item in hl_data:
highlight_nodes.extend(item.get('nodes', []))
highlight_edges.extend(item.get('edges', []))
pos = session.pos
# Draw Highlights Underneath
if highlight_nodes:
nx.draw_networkx_nodes(session.G, pos, nodelist=highlight_nodes, node_color='yellow', node_size=900, alpha=0.5)
if highlight_edges:
nx.draw_networkx_edges(session.G, pos, edgelist=highlight_edges, edge_color='yellow', width=6, alpha=0.5)
# 4. Draw Standard Edges
hard_edges = [(u,v) for (u,v) in edges_to_draw if session.G.edges[u,v].get('type') == 'hard']
soft_edges = [(u,v) for (u,v) in edges_to_draw if session.G.edges[u,v].get('type') == 'soft']
nx.draw_networkx_edges(session.G, pos, edgelist=hard_edges, edge_color='black', width=2, arrowstyle='-|>')
nx.draw_networkx_edges(session.G, pos, edgelist=soft_edges, edge_color='grey', width=2, style='dashed', arrowstyle='-|>')
# 5. Draw Nodes (Shapes and Colors based on Type/Agent)
for n, d in session.G.nodes(data=True):
x, y = pos[n]
lbl = d.get('label', str(n))
ntype = d.get('type', 'Function')
agents = d.get('agent', ['Unassigned'])
if isinstance(agents, str): agents = [agents]
# Determine Color (Primary Agent)
primary_agent = agents[0]
color = session.agents.get(primary_agent, 'white')
# Shape
marker = 's' if ntype == 'Function' else 'o'
# Manual scatter plot to handle mixed shapes/colors
plt.scatter(x, y, s=600, c=color, marker=marker, edgecolors='black', linewidth=1.5, zorder=2)
plt.text(x, y-40, lbl, ha='center', va='top', fontsize=9, fontweight='bold', zorder=3)
plt.axis('off')
plt.tight_layout()
return plt.gcf()
# --- Interaction Functions ---
def add_node_fn(label, n_type, layer, agent):
session.save_to_history()
# Generate ID
nid = session.counter
session.counter += 1
# Calculate position
y = config.JSAT_LAYERS.get(layer, 0)
x = random.randint(100, 900)
session.G.add_node(nid, label=label, type=n_type, layer=layer,
agent=[agent], pos=(x, y))
session.pos[nid] = (x, y)
return render_plot(), update_node_dropdown(), f"Added {label}"
def add_edge_fn(u_id, v_id, e_type):
if u_id is None or v_id is None:
return render_plot(), update_node_dropdown(), "Error: Select nodes"
# Enforce alternating type logic from Tkinter app
t1 = session.G.nodes[u_id].get('type')
t2 = session.G.nodes[v_id].get('type')
if t1 == t2:
return render_plot(), update_node_dropdown(), f"Error: Cannot connect {t1} to {t2}"
session.save_to_history()
session.G.add_edge(u_id, v_id, type=e_type)
return render_plot(), update_node_dropdown(), "Connection Created"
def delete_node_fn(u_id):
if u_id is None: return render_plot(), update_node_dropdown(), "No node selected"
session.save_to_history()
session.G.remove_node(u_id)
return render_plot(), update_node_dropdown(), "Node Deleted"
def create_agent_fn(name, color):
if not name: return "Name required", gr.update()
session.agents[name] = color
# Update choices for agent dropdowns
return f"Created agent {name}", gr.Dropdown(choices=list(session.agents.keys()))
def assign_agent_fn(node_id, agent_name):
if node_id is None: return render_plot(), "Select a node"
session.save_to_history()
current = session.G.nodes[node_id].get('agent', [])
if isinstance(current, str): current = [current]
# Logic from Tkinter: Toggle agent
if agent_name in current:
current.remove(agent_name)
else:
if "Unassigned" in current: current.remove("Unassigned")
current.append(agent_name)
if not current: current = ["Unassigned"]
session.G.nodes[node_id]['agent'] = current
return render_plot(), f"Agents for node {node_id}: {current}"
def update_node_dropdown():
# Helper to refresh dropdown options
choices = session.get_node_choices()
return gr.Dropdown(choices=choices)
def calculate_stats_fn():
report = "### Network Statistics\n"
metrics = [
"Density", "Cyclomatic Number", "Global Efficiency",
"Supportive Gain", "Brittleness Ratio", "Interdependence",
"Total Cycles", "Modularity"
]
for m in metrics:
try:
val = calculate_metric(session.G, m)
desc = METRIC_DESCRIPTIONS.get(m, "")
report += f"**{m}**: {val}\n> *{desc}*\n\n"
except Exception as e:
report += f"**{m}**: Error ({str(e)})\n\n"
return report
def snapshot_fn(name):
if not name: return "Enter a name", gr.update()
session.saved_snapshots[name] = session.G.copy()
return f"Saved snapshot: {name}", gr.update(choices=list(session.saved_snapshots.keys()))
def compare_fn(selected_snapshots):
if not selected_snapshots: return pd.DataFrame()
data = []
metrics = ["Density", "Global Efficiency", "Total Cycles", "Modularity"]
for name in selected_snapshots:
g = session.saved_snapshots[name]
row = {"Snapshot": name, "Nodes": g.number_of_nodes(), "Edges": g.number_of_edges()}
for m in metrics:
row[m] = calculate_metric(g, m)
data.append(row)
return pd.DataFrame(data)
def export_json_fn():
# Mimic the Save JSON logic
nodes_dict = {}
agent_authorities = {name: [] for name in session.agents}
for nid, d in session.G.nodes(data=True):
lbl = d.get('label', f"Node_{nid}")
layer = d.get('layer', "Base Environment").replace(" ", "")
typ = d.get('type', "Function")
nodes_dict[lbl] = {"Type": f"{layer}{typ}", "UserData": lbl}
ag_list = d.get('agent', ["Unassigned"])
if not isinstance(ag_list, list): ag_list = [ag_list]
for ag in ag_list:
if ag in agent_authorities: agent_authorities[ag].append(lbl)
edges_list = []
for u, v, d in session.G.edges(data=True):
src_lbl = session.G.nodes[u].get('label', str(u))
tgt_lbl = session.G.nodes[v].get('label', str(v))
edges_list.append({
"Source": src_lbl,
"Target": tgt_lbl,
"UserData": {"type": d.get('type', config.EDGE_TYPE_HARD)}
})
final = {"GraphData": {
"Nodes": nodes_dict,
"Edges": edges_list,
"Agents": {name: {"Authority": auth} for name, auth in agent_authorities.items()}
}}
# Return as string for Textbox
return json.dumps(final, indent=4)
def load_json_fn(json_str):
try:
data = json.loads(json_str)["GraphData"]
session.G.clear()
session.pos = {}
session.counter = 0
# Load Agents
for ag_name, ag_data in data.get("Agents", {}).items():
if ag_name not in session.agents:
session.agents[ag_name] = "#999999" # Default color if unknown
# Load Nodes
label_to_id = {}
for lbl, props in data.get("Nodes", {}).items():
combined_type = props.get("Type", "")
# Basic Parse logic
ntype = "Function" if "Function" in combined_type else "Resource"
# Find layer
layer = "Distributed Work"
for l in config.LAYER_ORDER:
if l.replace(" ","") in combined_type:
layer = l
break
nid = session.counter
session.counter += 1
# Position logic
y = config.JSAT_LAYERS.get(layer, 0)
x = random.randint(100, 900)
session.G.add_node(nid, label=lbl, type=ntype, layer=layer, agent=["Unassigned"], pos=(x,y))
session.pos[nid] = (x,y)
label_to_id[lbl] = nid
# Load Edges
for e in data.get("Edges", []):
u = label_to_id.get(e["Source"])
v = label_to_id.get(e["Target"])
if u is not None and v is not None:
etype = e.get("UserData", {}).get("type", "hard")
session.G.add_edge(u, v, type=etype)
return render_plot(), update_node_dropdown(), "JSON Loaded Successfully"
except Exception as e:
return render_plot(), update_node_dropdown(), f"Error loading JSON: {str(e)}"
# --- Layout Construction ---
with gr.Blocks(title="Interactive JSAT", theme=gr.themes.Soft()) as demo:
gr.Markdown("# πŸ•ΈοΈ Interactive JSAT Graph Builder")
with gr.Row():
# LEFT COLUMN: Visualization
with gr.Column(scale=2):
plot_output = gr.Plot(label="Network Architecture")
log_output = gr.Textbox(label="System Log", value="Ready.", interactive=False)
with gr.Row():
vis_mode = gr.Radio(["None", "Cycles", "Interdependence", "Modularity"], label="Visual Analytics Overlay", value="None")
edge_filter = gr.Radio(["ALL", "hard", "soft"], label="Show Edges", value="ALL")
# RIGHT COLUMN: Controls (Tabs)
with gr.Column(scale=1):
# --- TAB 1: EDITOR ---
with gr.Tab("πŸ“ Editor"):
gr.Markdown("### Add Node")
with gr.Row():
n_lbl = gr.Textbox(label="Label", placeholder="F1")
n_type = gr.Dropdown(["Function", "Resource"], label="Type", value="Function")
with gr.Row():
n_layer = gr.Dropdown(config.LAYER_ORDER, label="Layer", value="Distributed Work")
n_agent = gr.Dropdown(list(session.agents.keys()), label="Initial Agent", value="Unassigned")
btn_add_n = gr.Button("βž• Create Node", variant="primary")
gr.Markdown("### Connections")
with gr.Row():
# These dropdowns update dynamically
src_drop = gr.Dropdown(label="Source", choices=[])
tgt_drop = gr.Dropdown(label="Target", choices=[])
e_type = gr.Radio(["hard", "soft"], label="Constraint", value="hard")
btn_add_e = gr.Button("πŸ”— Connect", variant="secondary")
gr.Markdown("### Management")
del_node_drop = gr.Dropdown(label="Select Node to Delete", choices=[])
btn_del = gr.Button("πŸ—‘οΈ Delete Node", variant="stop")
# --- TAB 2: AGENTS ---
with gr.Tab("πŸ‘₯ Agents"):
gr.Markdown("### Create New Agent")
with gr.Row():
new_ag_name = gr.Textbox(label="Name")
new_ag_col = gr.ColorPicker(label="Color", value="#00ff00")
btn_create_ag = gr.Button("Save Agent")
gr.Markdown("### Assign to Node")
with gr.Row():
node_assign_drop = gr.Dropdown(label="Node", choices=[])
agent_assign_drop = gr.Dropdown(label="Agent", choices=list(session.agents.keys()))
btn_assign = gr.Button("Toggle Assignment")
# --- TAB 3: ANALYTICS ---
with gr.Tab("πŸ“Š Analytics"):
stats_box = gr.Markdown("Click 'Calculate' to see metrics...")
btn_stats = gr.Button("Calculate Metrics")
# --- TAB 4: COMPARE ---
with gr.Tab("βš–οΈ Compare"):
snap_name = gr.Textbox(label="Snapshot Name")
btn_snap = gr.Button("Save Snapshot")
snap_select = gr.CheckboxGroup(label="Select Snapshots to Compare", choices=[])
btn_compare = gr.Button("Generate Comparison Table")
compare_table = gr.Dataframe(label="Comparison Matrix")
# --- TAB 5: I/O ---
with gr.Tab("πŸ’Ύ I/O"):
btn_export = gr.Button("Generate JSON")
json_out = gr.Textbox(label="JSON Output", lines=5, show_copy_button=True)
gr.Markdown("---")
json_in = gr.Textbox(label="Paste JSON Here", lines=5)
btn_import = gr.Button("Load from JSON")
# --- Event Wiring ---
# Initialization
demo.load(render_plot, None, plot_output)
demo.load(update_node_dropdown, None, src_drop)
demo.load(update_node_dropdown, None, tgt_drop)
demo.load(update_node_dropdown, None, del_node_drop)
demo.load(update_node_dropdown, None, node_assign_drop)
# Visualization Triggers
vis_mode.change(render_plot, [vis_mode, edge_filter], plot_output)
edge_filter.change(render_plot, [vis_mode, edge_filter], plot_output)
# Editor Actions
btn_add_n.click(add_node_fn, [n_lbl, n_type, n_layer, n_agent], [plot_output, src_drop, log_output]) \
.then(update_node_dropdown, None, tgt_drop) \
.then(update_node_dropdown, None, del_node_drop) \
.then(update_node_dropdown, None, node_assign_drop)
btn_add_e.click(add_edge_fn, [src_drop, tgt_drop, e_type], [plot_output, src_drop, log_output])
btn_del.click(delete_node_fn, [del_node_drop], [plot_output, del_node_drop, log_output]) \
.then(update_node_dropdown, None, src_drop) \
.then(update_node_dropdown, None, tgt_drop)
# Agent Actions
btn_create_ag.click(create_agent_fn, [new_ag_name, new_ag_col], [log_output, n_agent]) \
.then(lambda: gr.Dropdown(choices=list(session.agents.keys())), None, agent_assign_drop)
btn_assign.click(assign_agent_fn, [node_assign_drop, agent_assign_drop], [plot_output, log_output])
# Analytics
btn_stats.click(calculate_stats_fn, None, stats_box)
# Comparison
btn_snap.click(snapshot_fn, snap_name, [log_output, snap_select])
btn_compare.click(compare_fn, snap_select, compare_table)
# I/O
btn_export.click(export_json_fn, None, json_out)
btn_import.click(load_json_fn, json_in, [plot_output, src_drop, log_output]) \
.then(update_node_dropdown, None, tgt_drop) \
.then(update_node_dropdown, None, del_node_drop)
if __name__ == "__main__":
demo.launch()