import gradio as gr
from typing import Any, Dict
import pandas as pd
import zipfile
import os
import tempfile
from analyze.inter_nodes import (
run_cross_analysis
)
from loaders import (
get_excel_or_json_data
)
from analyze.intra_node import (
PairwiseRelationshipEvaluator
)
# from cross_evaluator import CrossNodeRelationshipEvaluator, find_selected_path
from cross_generator import CrossNodeDataGenerator
from generator import SyntheticDataGenerator
def select_all_nodes(nodes):
return nodes
def update_node_selector(node_list):
return gr.update(choices=node_list)
def show_selected_edge(edge_name: str, edge_frames_state: dict[str, pd.DataFrame]):
if not edge_name or not edge_frames_state:
return pd.DataFrame()
df = edge_frames_state.get(edge_name)
if df is None:
return pd.DataFrame()
return df
def view_selected_node_analysis(selected_node, all_node_analysis):
if not selected_node:
return pd.DataFrame()
if all_node_analysis and all_node_analysis is not None:
df = all_node_analysis.get(selected_node)
if df is None:
return pd.DataFrame()
return df
return pd.DataFrame()
def generate_cross_node_data(
node_data_state: Dict[str, pd.DataFrame],
cross_node_analysis_dfs: pd.DataFrame,
selected_nodes: list[str],
):
try:
cross_gen = CrossNodeDataGenerator()
edge_frames = cross_gen.generate_edge_frames(
node_data_state=node_data_state,
selected_nodes=selected_nodes,
cross_node_analysis_dfs=cross_node_analysis_dfs,
max_edges=20,
max_rows_per_edge=50,
)
if not edge_frames:
return {}, gr.update(choices=[], value=None), pd.DataFrame(), ""
# Convert tuple keys to readable labels for the dropdown
edge_map: dict[str, pd.DataFrame] = {}
edge_choices: list[str] = []
for (parent_node, child_node), edge_df in edge_frames.items():
edge_name = f"{parent_node} -> {child_node}"
edge_map[edge_name] = edge_df
edge_choices.append(edge_name)
first_edge = edge_choices[0] if edge_choices else None
first_df = edge_map[first_edge] if first_edge else pd.DataFrame()
return (
edge_map,
gr.update(choices=edge_choices, value=first_edge),
first_df,
"",
)
except Exception as e:
print(f"[CROSS GENERATION ERROR] {e}")
return {}, gr.update(choices=[], value=None), pd.DataFrame(), f"
error while generating data - app
{str(e)}
"
def compute_cross_node_analysis(
weights_state,
schema_state,
selected_nodes,
node_data_state,
cross_node_analysis_dfs,
edges_out
):
try:
full_analysis: dict[str, pd.DataFrame] = {}
gr_selector = gr.update(choices=selected_nodes, value=selected_nodes[0] if selected_nodes else None)
filtered_analysis_df = cross_node_analysis_dfs[
cross_node_analysis_dfs["classification"].str.lower().isin(
["functional", "strong", "conditional"]
)
]
for _, row in edges_out.iterrows():
parent = row["parent"]
child = row["child"]
try:
# df = node_data_state.get(child)
# if df is None or df.empty:
# continue
# schema = next((s for s in schema_state if s.name == child), None)
# engine = PairwiseRelationshipEvaluator(schema, weights_state)
# results = engine.evaluate_all_pairs(df)
# mask = filtered_analysis_df["B"].astype(str).str.lower().str.startswith(child.lower() + ".")
# cross_node_validation = filtered_analysis_df[mask].copy().reset_index(drop=True)
# combined = pd.concat([results, cross_node_validation], ignore_index=True, sort=False)
# full_analysis[child] = combined
df = node_data_state.get(child)
# Default empty frames
results = pd.DataFrame()
cross_node_validation = pd.DataFrame()
if df is not None and not df.empty:
schema = next((s for s in schema_state if s.name == child), None)
if schema is not None:
engine = PairwiseRelationshipEvaluator(schema, weights_state)
results = engine.evaluate_all_pairs(df)
# cross-node filter (safe even if empty)
if not filtered_analysis_df.empty:
mask = filtered_analysis_df["B"].astype(str).str.lower().str.startswith(child.lower() + ".")
cross_node_validation = filtered_analysis_df[mask].copy().reset_index(drop=True)
# combine safely
if not results.empty or not cross_node_validation.empty:
combined = pd.concat([results, cross_node_validation], ignore_index=True, sort=False)
else:
# 👇 THIS is what you want
combined = pd.DataFrame()
# 👇 ALWAYS assign
full_analysis[child] = combined
except Exception as e:
print( f"[compute_node_analysis] {child} - {str(e)}")
full_analysis[child] = pd.DataFrame()
continue
return filtered_analysis_df, gr_selector, full_analysis, ""
except Exception as e:
return None, gr_selector, None, f"[compute_node_analysis] {str(e)}"
def create_cross_node_data(
schema_state,
node_data_state,
all_node_analysis,
selected_nodes,
edges_out,
num_rows = 50
):
full_data: dict[str, pd.DataFrame] = {}
gr_selector = gr.update(choices=selected_nodes, value=selected_nodes[0] if selected_nodes else None)
try:
for _, row in edges_out.iterrows():
parent = row["parent"]
child = row["child"]
schema = next((s for s in schema_state if s.name == child), None)
# print(f"schema {schema}")
df = node_data_state.get(child)
node_analysis = all_node_analysis.get(child)
if df is None:
print(f"Missing df for child={child}. Available keys: {list(node_data_state.keys())}")
return
if node_analysis is None:
print(f"Missing node_analysis for child={child}. Available keys: {list(all_node_analysis.keys())}")
return
# generate synthetic data
gen = SyntheticDataGenerator(
real_rows=df,
relationships=node_analysis,
schema=schema,
synth_df=full_data
)
synth_df = gen.generate(int(num_rows))
full_data[child] = synth_df
return full_data, gr_selector, ""
except Exception as e:
print(f"exception - generate_cross_node_data {str(e)}")
return None, [], f"[generate_cross_node_data] {str(e)}"
def log_full_node_data_shapes(full_node_data_state):
if not isinstance(full_node_data_state, dict) or not full_node_data_state:
print("full_node_data_state is empty or not a dict")
return
for node, df in full_node_data_state.items():
if df is None:
print(f"{node}: None")
else:
try:
print(f"{node}: {df.shape}")
except Exception as e:
print(f"{node}: could not read shape ({type(df)}), error={e}")
def download_selected_nodes(selected_nodes, all_node_data_dict):
# Create a temp zip file
zip_path = tempfile.mktemp(suffix=".zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
for node in selected_nodes:
node_df = all_node_data_dict.get(node)
if node_df is None:
continue
# Save each dataframe as CSV with column names at the top
csv_path = tempfile.mktemp(suffix=f"_{node}.csv")
node_df.to_csv(csv_path, index=False, header=True)
# Add to zip with a clean filename
zipf.write(csv_path, arcname=f"{node}.csv")
# Remove temp CSV
os.remove(csv_path)
return zip_path
def view_inter_nodes_analysis(
weights_state,
data_upload,
selected_node_table,
selected_node_dataframe,
schema_state,
node_list_state,
full_node_data_state,
edges,
edges_out,
error_box
):
print("full node data - view_inter_nodes_analysis")
log_full_node_data_shapes(full_node_data_state)
print("view_inter_nodes_analysis")
# cross node validation
gr.Markdown("## Cross Node Analysis")
with gr.Row():
select_all_btn = gr.Button("Select All Nodes")
uncheck_all_btn = gr.Button("Uncheck All Nodes")
node_selector = gr.CheckboxGroup(
label="Select Nodes for Cross Analysis",
choices=[], # ✅ must be list, not State
)
cross_btn = gr.Button("Run Cross Node Analysis")
select_all_btn.click(
fn=select_all_nodes,
inputs=[node_list_state],
outputs=node_selector,
)
uncheck_all_btn.click(
fn=select_all_nodes,
inputs=[],
outputs=node_selector,
)
gr.Markdown("### View Node Analysis")
cross_node_analysis_state = gr.State({})
cross_node_relationship_state = gr.State({})
cross_node_analysis_summary = gr.Markdown()
feature_table = gr.Dataframe()
cross_node_analysis_dfs = gr.Dataframe(label="Cross Node Relation", interactive=False)
cross_node_analysis_html_table = gr.HTML()
gr.Markdown("### Generate Cross Node Data")
gen_cross_node_btn = gr.Button("Gen Selected Node Data")
# cross_gen_html = gr.HTML()
edge_selector = gr.Dropdown(
label="Select Edge",
choices=[],
interactive=True,
)
cross_gen_table = gr.Dataframe(
label="Cross Node Generated Table",
interactive=False,
)
cross_gen_error = gr.HTML()
edge_frames_state = gr.State({})
cross_btn.click(
fn=run_cross_analysis,
inputs=[
weights_state,
schema_state,
full_node_data_state,
edges,
node_selector
],
outputs=[
cross_node_analysis_state,
cross_node_relationship_state,
cross_node_analysis_summary,
feature_table,
cross_node_analysis_dfs,
cross_node_analysis_html_table
]
)
# data upload
data_upload.change(
fn=get_excel_or_json_data,
inputs=[data_upload],
outputs=[
full_node_data_state,
selected_node_table,
selected_node_dataframe,
node_list_state,
error_box
],
).then(
fn=update_node_selector,
inputs=node_list_state,
outputs=node_selector
)
gen_cross_node_btn.click(
fn=generate_cross_node_data,
inputs=[
full_node_data_state,
cross_node_analysis_dfs,
node_selector,
],
outputs=[
edge_frames_state,
edge_selector,
cross_gen_table,
cross_gen_error,
],
)
edge_selector.change(
fn=show_selected_edge,
inputs=[
edge_selector,
edge_frames_state,
],
outputs=[
cross_gen_table,
],
)
# combine individual node and cross node data
gr.Markdown("### combine individual node and cross node data")
all_node_analysis_btn = gr.Button("Node Analyse Selected Nodes")
cross_node_analysis_selector = gr.Dropdown(
label="Select Node",
choices=[],
interactive=True,
)
nodes_analysis = gr.DataFrame()
all_node_analysis = gr.State({})
all_node_analysis_btn.click(
fn=compute_cross_node_analysis,
inputs=[
weights_state,
schema_state,
node_selector,
full_node_data_state,
cross_node_analysis_dfs,
edges_out
],
outputs=[
nodes_analysis,
cross_node_analysis_selector,
all_node_analysis,
error_box
]
)
cross_node_analysis_selector.change(
fn=view_selected_node_analysis,
inputs=[
cross_node_analysis_selector,
all_node_analysis,
],
outputs=[
nodes_analysis,
]
)
# edge_frames_state - cross node data (edge)
gen_all_data_btn = gr.Button("Gen Data for Selected Nodes")
cross_node_data_selector = gr.Dropdown(
label="Select Node Data",
choices=[],
interactive=True,
)
all_node_data = gr.State({})
view_node_data = gr.DataFrame()
gen_all_data_btn.click(
fn=create_cross_node_data,
inputs=[
schema_state,
full_node_data_state,
all_node_analysis,
node_selector,
edges_out
],
outputs=[
all_node_data,
cross_node_data_selector,
error_box
]
)
cross_node_data_selector.change(
fn=view_selected_node_analysis,
inputs=[
cross_node_data_selector,
all_node_data,
],
outputs=[
view_node_data,
]
)
download_btn = gr.Button("Download Selected Node Data")
download_file = gr.File(label="Download CSV")
download_btn.click(
fn=download_selected_nodes,
inputs=[node_selector, all_node_data],
outputs=download_file
)
return error_box