""" PTS Visualizer - Interactive visualization for Pivotal Token Search A Neuronpedia-inspired platform for exploring pivotal tokens, thought anchors, and reasoning circuits in language models. """ import gradio as gr import plotly.express as px import plotly.graph_objects as go import plotly.io as pio from plotly.subplots import make_subplots import networkx as nx import pandas as pd import numpy as np import json import html as html_lib from typing import List, Dict, Any, Optional, Tuple from datasets import load_dataset from sklearn.manifold import TSNE from sklearn.decomposition import PCA import re from collections import defaultdict # ============================================================================ # Data Loading Functions # ============================================================================ def load_hf_dataset(dataset_id: str, split: str = "train") -> pd.DataFrame: """Load a dataset from HuggingFace Hub. Reads the raw JSONL directly rather than going through ``datasets.load_dataset``. The events schema is deeply nested (link arrays with hundreds of entries, metadata dicts), and Arrow schema inference over that is slow -- ~15s for a 30 MB file vs ~2.5s for a direct download + parse. Falls back to the datasets library for anything not stored as a flat JSONL. """ from huggingface_hub import hf_hub_download, list_repo_files try: files = list_repo_files(dataset_id, repo_type="dataset") jsonl = [f for f in files if f.endswith(".jsonl")] if jsonl: # Prefer the canonical event file when present. preferred = next((f for f in jsonl if "causal_events" in f), jsonl[0]) path = hf_hub_download(dataset_id, preferred, repo_type="dataset") rows = [json.loads(l) for l in open(path) if l.strip()] return pd.DataFrame(rows), f"Loaded {len(rows)} items from {dataset_id}" dataset = load_dataset(dataset_id, split=split) df = pd.DataFrame(dataset) return df, f"Loaded {len(df)} items from {dataset_id}" except Exception as e: return pd.DataFrame(), f"Error loading dataset: {str(e)}" def load_jsonl_file(file_path: str) -> pd.DataFrame: """Load data from a local JSONL file.""" try: data = [] with open(file_path, 'r') as f: for line in f: if line.strip(): data.append(json.loads(line)) return pd.DataFrame(data), f"Loaded {len(data)} items from file" except Exception as e: return pd.DataFrame(), f"Error loading file: {str(e)}" def detect_dataset_type(df: pd.DataFrame) -> str: """Detect the type of PTS dataset. PTS emits a single unified record type (``CausalReasoningEvent``) that carries ``event_type`` + ``granularity``. Those are checked first; the v1 detection rules below them are unchanged so old datasets keep working. """ columns = set(df.columns) # --- PTS unified event schema ------------------------------------- if 'event_type' in columns: event_types = set() if not df.empty: event_types = { str(v) for v in df['event_type'].dropna().unique().tolist() } # A file containing nothing but latent meta-tokens (e.g. `pts export # --format=metatokens`) gets its own type so the UI can lead with the # workspace views instead of the probability views. if event_types and event_types == {EVENT_LATENT}: return 'latent_events' if 'granularity' in columns: return 'causal_events' # --- PTS v1 ---------------------------------------------------------- if 'sentence' in columns and 'sentence_id' in columns: return 'thought_anchors' elif 'steering_vector' in columns: return 'steering_vectors' elif 'chosen' in columns and 'rejected' in columns: return 'dpo_pairs' elif 'pivot_token' in columns: return 'pivotal_tokens' else: return 'unknown' # ============================================================================ # PTS event schema: constants and helpers # ============================================================================ EVENT_LATENT = "latent_metatoken" EVENT_TOKEN = "pivotal_token" EVENT_SENTENCE = "thought_anchor" GRANULARITY_FOR_EVENT_TYPE = { EVENT_LATENT: "latent", EVENT_TOKEN: "token", EVENT_SENTENCE: "sentence", } PTS_TYPES = ('causal_events', 'latent_events') # The design system lives at the bottom of this file (see "Design system"), but # the palette is needed up here by the chart helpers. Two axes, kept strictly # apart: # # CHANNEL which scale an event belongs to (latent / token / sentence) # VALENCE which way it moved success (positive / negative) # # Latent events have a channel but NO valence -- prob_delta is null by # construction -- so they must never be painted green or red. Violet means # "observed in the workspace, direction unknown", and that is the whole point. C_CANVAS = '#0A0D13' C_PANEL = '#111722' C_PANEL_HI = '#161E2C' C_LINE = '#212C3D' C_LINE_SOFT = '#1A2333' C_TEXT = '#CBD5E4' C_TEXT_MUTED = '#A6B2C7' C_TEXT_FAINT = '#8592A8' C_LATENT = '#A78BFA' # violet -- hidden, deep C_TOKEN = '#38BDF8' # sky -- emitted, a single decision point C_SENTENCE = '#FBBF24' # amber -- emitted, an extended step C_POSITIVE = '#34D399' # emerald C_NEGATIVE = '#FB7185' # rose C_NEUTRAL = '#64748B' COLOR_POSITIVE = C_POSITIVE COLOR_NEGATIVE = C_NEGATIVE COLOR_LATENT = C_LATENT COLOR_UNKNOWN = C_NEUTRAL COLOR_OUTCOME = C_SENTENCE CHANNEL = {'latent': C_LATENT, 'token': C_TOKEN, 'sentence': C_SENTENCE} CATEGORY_COLORS = [ C_LATENT, C_TOKEN, C_SENTENCE, C_POSITIVE, C_NEGATIVE, '#F472B6', '#2DD4BF', '#FB923C', '#818CF8', '#A3E635', ] def is_pts_events(df: pd.DataFrame) -> bool: """True when the dataframe holds PTS unified events.""" return not df.empty and 'event_type' in df.columns def as_pts_events(df: pd.DataFrame) -> pd.DataFrame: """Present a v1 dataframe as PTS events, in memory. This is the whole PTS thesis applied to the UI: a legacy pivotal-token file *is* a token-scale event stream, and a legacy thought-anchor file *is* a sentence-scale one. They differ only in what the columns are called. Upgrading them here means the reasoning views work on every published dataset instead of showing an empty box until someone re-exports. Returns the frame unchanged if it is already a PTS event frame, or neither shape (steering vectors, DPO pairs). """ if df.empty or is_pts_events(df): return df out = df.copy() cols = set(out.columns) if 'pivot_token' in cols: out['event_type'] = 'pivotal_token' out['granularity'] = 'token' out['visibility'] = 'emitted' out['label'] = out['pivot_token'] out['context'] = out.get('pivot_context', '') if 'pivot_token_id' in cols: out['token_id'] = out['pivot_token_id'] elif {'sentence', 'sentence_id'} <= cols: out['event_type'] = 'thought_anchor' out['granularity'] = 'sentence' out['visibility'] = 'emitted' out['label'] = out['sentence'] out['context'] = out.get('prefix_context', '') out['position'] = out['sentence_id'] # legacy named these prob_with/prob_without; PTS calls them after/before. if 'prob_with_sentence' in cols: out['prob_after'] = out['prob_with_sentence'] if 'prob_without_sentence' in cols: out['prob_before'] = out['prob_without_sentence'] if 'sentence_category' in cols: out['category'] = out['sentence_category'] else: return df if 'prob_delta' in out.columns: out['score'] = out['prob_delta'].abs() if 'is_positive' not in out.columns: out['is_positive'] = out['prob_delta'] > 0 for c in ('layer', 'readout_method', 'linked_event_ids', 'precedes_event_ids', 'follows_event_ids', 'parent_event_id', 'event_id'): if c not in out.columns: out[c] = None if out['event_id'].isna().all(): out['event_id'] = [f'v1_evt_{i}' for i in range(len(out))] return out def _empty_fig(message: str, height: int = 400) -> go.Figure: """A placeholder that reads as a deliberate state, not a broken chart.""" fig = go.Figure() fig.add_annotation( text=message, xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(family="'IBM Plex Mono', monospace", size=12, color=C_TEXT_MUTED), align="center", bgcolor=C_PANEL, bordercolor=C_LINE, borderwidth=1, borderpad=14, ) fig.update_layout( template="pts", height=height, xaxis=dict(visible=False), yaxis=dict(visible=False), ) return fig def _val(row, key, default=None): """Read a field off a row, mapping NaN/None/missing onto ``default``.""" try: value = row.get(key, default) except AttributeError: return default if value is None: return default try: if pd.isna(value): return default except (TypeError, ValueError): # pd.isna over a list/array returns an array; those are real values. pass return value def _as_list(value) -> list: """Normalize a link field (list / ndarray / None / scalar) into a list.""" if value is None: return [] if isinstance(value, (list, tuple, set)): return [v for v in value if v is not None] if isinstance(value, np.ndarray): return [v for v in value.tolist() if v is not None] if isinstance(value, str): return [value] if value else [] try: if pd.isna(value): return [] except (TypeError, ValueError): pass return [value] def _num(value, default=0.0) -> float: try: out = float(value) except (TypeError, ValueError): return default if np.isnan(out): return default return out def _granularity(row) -> str: gran = _val(row, 'granularity') if gran: return str(gran) return GRANULARITY_FOR_EVENT_TYPE.get(str(_val(row, 'event_type', '')), 'token') def _granularity_series(df: pd.DataFrame) -> pd.Series: """Vectorized granularity for a whole frame. ``_granularity_series(df)`` is a row-wise Python loop -- seconds over 14k events, and it ran on every dataset load. The data already has a ``granularity`` column, so this is a column read with an event_type fallback. """ if 'granularity' in df.columns: g = df['granularity'].astype('object').where(df['granularity'].notna(), None) else: g = pd.Series([None] * len(df), index=df.index, dtype='object') if 'event_type' in df.columns: fallback = df['event_type'].map(lambda t: GRANULARITY_FOR_EVENT_TYPE.get(str(t), 'token')) g = g.where(g.notna(), fallback) return g.fillna('token').astype(str) def _event_color(row) -> str: """Positive=green, negative=red, latent/unscored=purple-blue. Never colors a latent event green or red: it has no measured valence. """ if _granularity(row) == 'latent': return COLOR_LATENT is_positive = _val(row, 'is_positive') if is_positive is None: delta = _val(row, 'prob_delta') if delta is None: return COLOR_UNKNOWN is_positive = _num(delta) > 0 return COLOR_POSITIVE if bool(is_positive) else COLOR_NEGATIVE def _score_label(row) -> str: """Name the score honestly: latent scores are readout scores, not deltas.""" if _granularity(row) == 'latent': return "Readout score" return "Score (|Δ probability|)" def _filter_by_query(df: pd.DataFrame, selected_query: Optional[str]) -> pd.DataFrame: if (selected_query and isinstance(selected_query, str) and selected_query.strip() and 'query' in df.columns): return df[df['query'] == selected_query].copy() return df def _default_query(df: pd.DataFrame, selected_query: Optional[str]) -> Optional[str]: """Resolve a per-query view to one query when none is chosen. The causal graph and timeline are per-query by nature -- laying out every event from every query at once is both meaningless and, on an enriched dataset (14k events, 69k links), pathologically slow (a spring layout over the whole graph takes minutes). So a per-query view with no selection shows the first query, not the entire dataset. """ if selected_query and isinstance(selected_query, str) and selected_query.strip(): return selected_query if 'query' in df.columns and len(df): return str(df['query'].iloc[0]) return selected_query def _event_hover(row) -> str: """Hover text shared by the charts.""" label = str(_val(row, 'label', _val(row, 'pivot_token', _val(row, 'sentence', '')))) if len(label) > 90: label = label[:87] + '...' parts = [ f"{html_lib.escape(label)}", f"Event: {_val(row, 'event_type', 'unknown')}", f"Granularity: {_granularity(row)}", f"Category: {_val(row, 'category', 'n/a')}", f"{_score_label(row)}: {_num(_val(row, 'score')):.4f}", ] if _granularity(row) == 'latent': parts.append(f"Layer: {_val(row, 'layer', 'n/a')}") parts.append(f"Readout: {_val(row, 'readout_method', 'n/a')}") parts.append("observational - no probability delta") else: delta = _val(row, 'prob_delta') if delta is not None: parts.append(f"Δ probability: {_num(delta):+.4f}") parts.append( f"Before: {_num(_val(row, 'prob_before')):.3f} → " f"After: {_num(_val(row, 'prob_after')):.3f}" ) return "
".join(parts) def _compute_event_x(df: pd.DataFrame) -> List[float]: """Place every event on one shared x-axis (generation position / event order). Emitted events use their ``position`` (token index or sentence index), falling back to their order of appearance. Latent enrichment events carry a *negative* ``position``: an offset in tokens from the event they were read out before. Those are anchored to the linked event's x, so a latent event with position -3 lands three steps to the left of the token it precedes. """ if df.empty: return [] rows = list(df.iterrows()) x_by_id: Dict[Any, float] = {} xs: List[Optional[float]] = [None] * len(rows) # Pass 1: emitted events anchor the axis. emitted_counter = 0 for i, (_, row) in enumerate(rows): if _granularity(row) == 'latent': continue pos = _val(row, 'position') if pos is None or _num(pos, -1) < 0: x = float(emitted_counter) else: x = float(_num(pos)) emitted_counter += 1 xs[i] = x event_id = _val(row, 'event_id') if event_id is not None: x_by_id[event_id] = x known = [x for x in xs if x is not None] floor = min(known) if known else 0.0 # Pass 2: latent events hang off the emitted event they precede. latent_counter = 0 for i, (_, row) in enumerate(rows): if _granularity(row) != 'latent': continue pos = _val(row, 'position') links = (_as_list(_val(row, 'precedes_event_ids')) + _as_list(_val(row, 'linked_event_ids'))) anchor = next((x_by_id[l] for l in links if l in x_by_id), None) if anchor is not None: offset = _num(pos, 0.0) if pos is not None and _num(pos, 0.0) < 0 else 0.0 xs[i] = anchor + offset elif pos is not None and _num(pos, -1) >= 0: xs[i] = float(_num(pos)) else: # Unlinked, offset-only latent event: park it to the left of the # emitted timeline rather than pretending it has a position. xs[i] = floor - 1.0 - latent_counter * 0.1 latent_counter += 1 return [float(x) if x is not None else 0.0 for x in xs] # ============================================================================ # Visualization Components # ============================================================================ def create_token_highlight_html(context: str, token: str, prob_delta: float) -> str: """Create HTML with highlighted pivotal token showing full context.""" # Escape HTML characters context_escaped = html_lib.escape(str(context)) token_escaped = html_lib.escape(str(token)) # Determine color based on probability delta if prob_delta > 0: # Positive impact - green gradient intensity = min(abs(prob_delta) * 2, 1.0) color = f"rgba(34, 197, 94, {intensity})" border_color = "#34D399" impact_text = "Positive Impact" else: # Negative impact - red gradient intensity = min(abs(prob_delta) * 2, 1.0) color = f"rgba(239, 68, 68, {intensity})" border_color = "#FB7185" impact_text = "Negative Impact" # Create highlighted token span token_span = f'{token_escaped}' return f"""
Context Length: {len(context)} characters {impact_text}: {'+' if prob_delta > 0 else ''}{prob_delta:.3f}
{context_escaped}{token_span}
Token: {token_escaped}
""" def create_probability_chart(prob_before: float, prob_after: float) -> go.Figure: """Create a bar chart showing probability change.""" fig = go.Figure() # Ensure values are Python floats prob_before = float(prob_before) if prob_before is not None else 0.0 prob_after = float(prob_after) if prob_after is not None else 0.0 fig.add_trace(go.Bar( x=['Before Token', 'After Token'], y=[prob_before, prob_after], marker_color=[C_NEUTRAL, C_POSITIVE if prob_after > prob_before else C_NEGATIVE], text=[f'{prob_before:.3f}', f'{prob_after:.3f}'], textposition='outside' )) fig.update_layout( title="Success Probability Change", yaxis_title="Probability", yaxis_range=[0, 1], template="pts", height=300 ) return fig def create_pivotal_token_flow(df: pd.DataFrame, selected_query: str = None) -> go.Figure: """Create a visualization for pivotal tokens showing token impact flow.""" if df.empty: fig = go.Figure() fig.add_annotation(text="No data available", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) fig.update_layout(template="pts") return fig if 'prob_delta' not in df.columns: return _empty_fig( "This dataset has no prob_delta column, so token impact " "cannot be plotted." ) # Filter by query if specified (handle None, empty string, or actual query) if selected_query and isinstance(selected_query, str) and selected_query.strip() and 'query' in df.columns: df = df[df['query'] == selected_query].copy() if df.empty: fig = go.Figure() fig.add_annotation(text="No data for selected query", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) fig.update_layout(template="pts") return fig # Create scatter plot of tokens by probability delta fig = go.Figure() # Separate positive and negative tokens positive_df = df[df.get('is_positive', df['prob_delta'] > 0) == True] if 'is_positive' in df.columns else df[df['prob_delta'] > 0] negative_df = df[df.get('is_positive', df['prob_delta'] > 0) == False] if 'is_positive' in df.columns else df[df['prob_delta'] <= 0] # Add positive tokens if not positive_df.empty: hover_text = [ f"Token: {row.get('pivot_token', 'N/A')}
" f"Δ Prob: +{row.get('prob_delta', 0):.3f}
" f"Before: {row.get('prob_before', 0):.3f}
" f"After: {row.get('prob_after', 0):.3f}
" f"Query: {str(row.get('query', ''))[:50]}..." for _, row in positive_df.iterrows() ] y_vals = positive_df['prob_delta'].tolist() sizes = [10 + abs(v) * 30 for v in y_vals] fig.add_trace(go.Scatter( x=list(range(len(positive_df))), y=y_vals, mode='markers', name='Positive Impact', marker=dict( size=sizes, color=C_POSITIVE, opacity=0.7 ), hovertext=hover_text, hoverinfo='text' )) # Add negative tokens if not negative_df.empty: hover_text = [ f"Token: {row.get('pivot_token', 'N/A')}
" f"Δ Prob: {row.get('prob_delta', 0):.3f}
" f"Before: {row.get('prob_before', 0):.3f}
" f"After: {row.get('prob_after', 0):.3f}
" f"Query: {str(row.get('query', ''))[:50]}..." for _, row in negative_df.iterrows() ] y_vals = negative_df['prob_delta'].tolist() sizes = [10 + abs(v) * 30 for v in y_vals] fig.add_trace(go.Scatter( x=list(range(len(negative_df))), y=y_vals, mode='markers', name='Negative Impact', marker=dict( size=sizes, color=C_NEGATIVE, opacity=0.7 ), hovertext=hover_text, hoverinfo='text' )) fig.add_hline(y=0, line_dash="dash", line_color="gray") fig.update_layout( title="Pivotal Token Impact Distribution", xaxis_title="Token Index", yaxis_title="Probability Delta", template="pts", height=500, showlegend=True ) return fig def create_thought_anchor_graph(df: pd.DataFrame, selected_query: str = None) -> go.Figure: """Create an interactive graph visualization of thought anchor dependencies.""" dataset_type = detect_dataset_type(df) # PTS events carry their own causal edges; use the unified graph. if dataset_type in PTS_TYPES: return create_causal_event_graph(df, selected_query) # For pivotal tokens and steering vectors, create a token impact visualization if dataset_type in ('pivotal_tokens', 'steering_vectors'): return create_pivotal_token_flow(df, selected_query) if df.empty or 'sentence_id' not in df.columns: fig = go.Figure() fig.add_annotation(text="No thought anchor data available. Load a thought anchors dataset to see the reasoning graph.", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=14, color="#a0a0a0")) fig.update_layout(template="pts", height=400) return fig # Filter by query if specified (handle None, empty string, or actual query) if selected_query and isinstance(selected_query, str) and selected_query.strip(): df = df[df['query'] == selected_query].copy() if df.empty: fig = go.Figure() fig.add_annotation(text="No data for selected query", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) fig.update_layout(template="pts") return fig # Create networkx graph G = nx.DiGraph() # Add nodes (sentences) for idx, row in df.iterrows(): sentence_id = row.get('sentence_id', idx) importance = row.get('importance_score', abs(row.get('prob_delta', 0))) is_positive = row.get('is_positive', row.get('prob_delta', 0) > 0) sentence = row.get('sentence', '')[:50] + '...' if len(row.get('sentence', '')) > 50 else row.get('sentence', '') G.add_node(sentence_id, importance=importance, is_positive=is_positive, sentence=sentence, category=row.get('sentence_category', 'unknown')) # Add edges from causal dependencies for idx, row in df.iterrows(): sentence_id = row.get('sentence_id', idx) dependencies = row.get('causal_dependencies', []) if isinstance(dependencies, list): for dep in dependencies: if dep in G.nodes(): G.add_edge(dep, sentence_id) # If no explicit dependencies, create sequential edges if G.number_of_edges() == 0: sorted_nodes = sorted(G.nodes()) for i in range(len(sorted_nodes) - 1): G.add_edge(sorted_nodes[i], sorted_nodes[i+1]) # Layout pos = nx.spring_layout(G, k=2, iterations=50) # Create edge traces edge_x = [] edge_y = [] for edge in G.edges(): x0, y0 = pos[edge[0]] x1, y1 = pos[edge[1]] edge_x.extend([float(x0), float(x1), None]) edge_y.extend([float(y0), float(y1), None]) edge_trace = go.Scatter( x=edge_x, y=edge_y, line=dict(width=1, color=C_LINE), hoverinfo='none', mode='lines' ) # Create node traces node_x = [] node_y = [] node_colors = [] node_sizes = [] node_texts = [] for node in G.nodes(): x, y = pos[node] node_x.append(float(x)) node_y.append(float(y)) node_data = G.nodes[node] is_positive = node_data.get('is_positive', True) importance = float(node_data.get('importance', 0.3)) node_colors.append(C_POSITIVE if is_positive else C_NEGATIVE) node_sizes.append(20 + importance * 50) hover_text = f"Sentence {node}
" hover_text += f"Category: {node_data.get('category', 'unknown')}
" hover_text += f"Importance: {importance:.3f}
" hover_text += f"Text: {node_data.get('sentence', 'N/A')}" node_texts.append(hover_text) node_trace = go.Scatter( x=node_x, y=node_y, mode='markers+text', hoverinfo='text', text=[str(n) for n in G.nodes()], textposition="top center", hovertext=node_texts, marker=dict( color=node_colors, size=node_sizes, line=dict(width=2, color='white') ) ) # Create figure fig = go.Figure(data=[edge_trace, node_trace]) fig.update_layout( title="Thought Anchor Reasoning Graph", showlegend=False, hovermode='closest', template="pts", xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), height=500 ) return fig # ============================================================================ # PTS: reasoning timeline, causal event graph, workspace heatmap # ============================================================================ def create_reasoning_timeline(df: pd.DataFrame, selected_query: str = None) -> go.Figure: """Four scales of reasoning events on one shared generation axis. Row 1 latent meta-tokens (diamonds, y = layer, size = readout score) Row 2 emitted pivotal tokens (circles, y = Δ probability) Row 3 thought-anchor sentences (wide bars, height = Δ probability) Row 4 success probability (from prob_before/prob_after of emitted events) """ if df is None or df.empty: return _empty_fig("No data loaded. Load a PTS dataset to see the reasoning timeline.", 640) # A legacy pivotal-token file IS a token-scale event stream, and a v1 # thought-anchor file IS a sentence-scale one -- that is the whole PTS # thesis. So upgrade them here rather than refusing, and every published # dataset renders instead of showing an empty box. Latent rows will simply # be absent until the file is enriched. df = as_pts_events(df) if not is_pts_events(df): return _empty_fig( "This view needs reasoning events.
" "Steering-vector and DPO files contain none.", 640, ) work = _filter_by_query(df, selected_query) if work.empty: return _empty_fig("No events for the selected query.", 640) work = work.reset_index(drop=True) xs = _compute_event_x(work) work = work.assign(_x=xs) gran = _granularity_series(work) latent_df = work[gran == 'latent'] token_df = work[gran == 'token'] sentence_df = work[gran == 'sentence'] fig = make_subplots( rows=4, cols=1, shared_xaxes=True, vertical_spacing=0.06, row_heights=[0.24, 0.22, 0.22, 0.32], subplot_titles=( "Latent meta-tokens (readout score - NOT a probability delta)", "Emitted pivotal tokens (Δ success probability)", "Thought-anchor sentences (Δ success probability)", "Success probability across emitted events", ), ) # --- Row 1: latent meta-tokens ------------------------------------- if not latent_df.empty: scores = [_num(_val(r, 'score')) for _, r in latent_df.iterrows()] max_score = max(scores) if scores else 1.0 max_score = max_score if max_score > 0 else 1.0 fig.add_trace( go.Scatter( x=latent_df['_x'].tolist(), y=[_num(_val(r, 'layer')) for _, r in latent_df.iterrows()], mode='markers', name='Latent meta-token', marker=dict( symbol='diamond', size=[6 + 10 * (s / max_score) for s in scores], color=COLOR_LATENT, opacity=0.85, line=dict(width=1, color=C_LATENT), ), hovertext=[_event_hover(r) for _, r in latent_df.iterrows()], hoverinfo='text', ), row=1, col=1, ) else: fig.add_annotation( text="no latent meta-token events in this dataset", xref="x domain", yref="y domain", x=0.5, y=0.5, showarrow=False, font=dict(size=11, color="#6b7280"), row=1, col=1, ) # --- Row 2: emitted pivotal tokens --------------------------------- if not token_df.empty: fig.add_trace( go.Scatter( x=token_df['_x'].tolist(), y=[_num(_val(r, 'prob_delta')) for _, r in token_df.iterrows()], mode='markers', name='Pivotal token', marker=dict( symbol='circle', size=[10 + 25 * abs(_num(_val(r, 'score'))) for _, r in token_df.iterrows()], color=[_event_color(r) for _, r in token_df.iterrows()], opacity=0.8, line=dict(width=1, color=C_CANVAS), ), hovertext=[_event_hover(r) for _, r in token_df.iterrows()], hoverinfo='text', ), row=2, col=1, ) fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1) else: fig.add_annotation( text="no emitted pivotal-token events", xref="x domain", yref="y domain", x=0.5, y=0.5, showarrow=False, font=dict(size=11, color="#6b7280"), row=2, col=1, ) # --- Row 3: thought-anchor sentences ------------------------------- if not sentence_df.empty: deltas = [_num(_val(r, 'prob_delta')) for _, r in sentence_df.iterrows()] # A sentence with no measured delta still deserves a bar: fall back to # its score, drawn in neutral blue rather than green/red. heights = [ d if d != 0 else _num(_val(r, 'score')) for d, (_, r) in zip(deltas, sentence_df.iterrows()) ] fig.add_trace( go.Bar( x=sentence_df['_x'].tolist(), y=heights, name='Thought anchor', width=0.8, marker=dict( color=[_event_color(r) for _, r in sentence_df.iterrows()], opacity=0.65, line=dict(width=1, color=C_CANVAS), ), hovertext=[_event_hover(r) for _, r in sentence_df.iterrows()], hoverinfo='text', ), row=3, col=1, ) fig.add_hline(y=0, line_dash="dash", line_color="gray", row=3, col=1) else: fig.add_annotation( text="no thought-anchor sentence events", xref="x domain", yref="y domain", x=0.5, y=0.5, showarrow=False, font=dict(size=11, color="#6b7280"), row=3, col=1, ) # --- Row 4: success probability curve ------------------------------ emitted = work[gran != 'latent'] curve_x: List[float] = [] curve_y: List[float] = [] curve_hover: List[str] = [] if not emitted.empty: ordered = emitted.sort_values('_x') for _, row in ordered.iterrows(): before = _val(row, 'prob_before') after = _val(row, 'prob_after') if before is None or after is None: continue x = float(row['_x']) curve_x.extend([x, x]) curve_y.extend([_num(before), _num(after)]) hover = _event_hover(row) curve_hover.extend([f"before
{hover}", f"after
{hover}"]) if curve_x: fig.add_trace( go.Scatter( x=curve_x, y=curve_y, mode='lines+markers', name='Success probability', line=dict(color=COLOR_UNKNOWN, width=2), marker=dict(size=7, color=COLOR_UNKNOWN), hovertext=curve_hover, hoverinfo='text', ), row=4, col=1, ) fig.add_hline(y=0.5, line_dash="dash", line_color="gray", row=4, col=1) else: fig.add_annotation( text="no prob_before/prob_after on any emitted event", xref="x domain", yref="y domain", x=0.5, y=0.5, showarrow=False, font=dict(size=11, color="#6b7280"), row=4, col=1, ) fig.update_yaxes(title_text="Layer", row=1, col=1) fig.update_yaxes(title_text="Δ prob", row=2, col=1) fig.update_yaxes(title_text="Δ prob", row=3, col=1) fig.update_yaxes(title_text="P(success)", range=[0, 1], row=4, col=1) fig.update_xaxes(title_text="Generation position / event order", row=4, col=1) fig.update_layout( title="Reasoning Timeline", template="pts", height=760, showlegend=True, hovermode='closest', bargap=0.2, ) return fig def create_causal_event_graph(df: pd.DataFrame, selected_query: str = None) -> go.Figure: """Causal graph over unified events: latent -> token -> sentence -> outcome. Edges come from ``precedes_event_ids`` / ``linked_event_ids`` / ``parent_event_id``. Node shape encodes granularity, node color encodes valence (latent nodes stay purple - they have no valence). """ if df is None or df.empty: return _empty_fig("No data loaded. Load a PTS dataset to see the causal event graph.", 550) # legacy datasets keep the old graph exactly as it was. if not is_pts_events(df): return create_thought_anchor_graph(df, selected_query) # Per-query view: never lay out the whole dataset (minutes on 14k nodes). selected_query = _default_query(df, selected_query) work = _filter_by_query(df, selected_query) if work.empty: return _empty_fig("No events for the selected query.", 550) work = work.reset_index(drop=True) xs = _compute_event_x(work) work = work.assign(_x=xs) G = nx.DiGraph() id_by_index: Dict[int, str] = {} for i, (_, row) in enumerate(work.iterrows()): event_id = _val(row, 'event_id') or f"event_{i}" id_by_index[i] = event_id label = str(_val(row, 'label', '')) G.add_node( event_id, label=label[:60] + ('...' if len(label) > 60 else ''), granularity=_granularity(row), event_type=str(_val(row, 'event_type', 'unknown')), category=_val(row, 'category', 'unknown'), score=_num(_val(row, 'score')), color=_event_color(row), hover=_event_hover(row), x_hint=float(row['_x']), ) for i, (_, row) in enumerate(work.iterrows()): source = id_by_index[i] targets = set(_as_list(_val(row, 'precedes_event_ids'))) targets |= set(_as_list(_val(row, 'linked_event_ids'))) for target in targets: if target in G.nodes() and target != source: G.add_edge(source, target) parent = _val(row, 'parent_event_id') if parent and parent in G.nodes() and parent != source: G.add_edge(parent, source) # No links attached (e.g. `pts link` was never run): fall back to # generation order so the graph still shows the reasoning sequence. if G.number_of_edges() == 0 and G.number_of_nodes() > 1: ordered = sorted(G.nodes(), key=lambda n: G.nodes[n]['x_hint']) for a, b in zip(ordered, ordered[1:]): G.add_edge(a, b) # Terminal outcome node: everything that leads nowhere leads to the outcome. OUTCOME = "__outcome__" leaves = [n for n in G.nodes() if G.out_degree(n) == 0] if leaves: G.add_node( OUTCOME, label="outcome", granularity='outcome', event_type='outcome', category='n/a', score=1.0, color=COLOR_OUTCOME, hover="Task outcome
(terminal node: success / failure of the completion)", x_hint=max((G.nodes[n]['x_hint'] for n in G.nodes() if n != OUTCOME), default=0.0) + 1.0, ) for leaf in leaves: G.add_edge(leaf, OUTCOME) pos = nx.spring_layout(G, k=1.6, iterations=60, seed=42) edge_x: List[Optional[float]] = [] edge_y: List[Optional[float]] = [] for a, b in G.edges(): x0, y0 = pos[a] x1, y1 = pos[b] edge_x.extend([float(x0), float(x1), None]) edge_y.extend([float(y0), float(y1), None]) traces = [go.Scatter( x=edge_x, y=edge_y, # Semi-transparent so a dense single-query graph reads as a web rather # than a solid mass, but light enough to actually be visible against the # near-black canvas -- C_LINE (#212C3D) is the panel-border colour and # was indistinguishable from the background. line=dict(width=1, color='rgba(148,163,190,0.45)'), hoverinfo='none', mode='lines', showlegend=False, )] symbols = { 'latent': 'diamond', 'token': 'circle', 'sentence': 'square', 'outcome': 'star', } names = { 'latent': 'Latent meta-token', 'token': 'Pivotal token', 'sentence': 'Thought anchor', 'outcome': 'Outcome', } for gran, symbol in symbols.items(): nodes = [n for n in G.nodes() if G.nodes[n]['granularity'] == gran] if not nodes: continue traces.append(go.Scatter( x=[float(pos[n][0]) for n in nodes], y=[float(pos[n][1]) for n in nodes], mode='markers', name=names[gran], marker=dict( symbol=symbol, size=[14 + 30 * min(G.nodes[n]['score'], 1.0) for n in nodes], color=[G.nodes[n]['color'] for n in nodes], line=dict(width=1.5, color='white'), ), hovertext=[G.nodes[n]['hover'] for n in nodes], hoverinfo='text', )) fig = go.Figure(data=traces) fig.update_layout( title="Causal Event Graph (latent → token → sentence → outcome)", showlegend=True, hovermode='closest', template="pts", xaxis=dict(showgrid=False, zeroline=False, showticklabels=False), yaxis=dict(showgrid=False, zeroline=False, showticklabels=False), height=600, ) return fig def create_workspace_heatmap(df: pd.DataFrame, selected_query: str = None) -> go.Figure: """Latent workspace: meta-token (or category) x position, colored by readout score. The color scale is a *readout* score (how strongly the lens surfaces that meta-token), never a probability delta. """ if df is None or df.empty: return _empty_fig("No data loaded. Load a PTS dataset with latent events.", 500) df = as_pts_events(df) if not is_pts_events(df): return _empty_fig( "The workspace heatmap needs latent meta-token events (PTS).
" "Generate them with pts run --scale=latent or " "pts enrich.", 500, ) work = _filter_by_query(df, selected_query) latent = work[_granularity_series(work) == 'latent'] if not work.empty else work if latent.empty: return _empty_fig( "No latent meta-token events in this selection.
" "The workspace heatmap only applies to latent_metatoken events.", 500, ) latent = latent.reset_index(drop=True) latent = latent.assign(_x=_compute_event_x(latent)) labels = [str(_val(r, 'label', '')) for _, r in latent.iterrows()] unique_labels = sorted(set(labels)) # Too many distinct meta-tokens to read as rows: fall back to categories. row_key = 'meta-token' if len(unique_labels) > 30 and 'category' in latent.columns: keys = [str(_val(r, 'category', 'unknown')) for _, r in latent.iterrows()] row_key = 'category' else: keys = labels xs = [float(x) for x in latent['_x'].tolist()] scores = [_num(_val(r, 'score')) for _, r in latent.iterrows()] layers = [_val(r, 'layer', 'n/a') for _, r in latent.iterrows()] readouts = [_val(r, 'readout_method', 'n/a') for _, r in latent.iterrows()] row_values = sorted(set(keys)) col_values = sorted(set(xs)) row_index = {v: i for i, v in enumerate(row_values)} col_index = {v: i for i, v in enumerate(col_values)} z = np.full((len(row_values), len(col_values)), np.nan) hover = [["" for _ in col_values] for _ in row_values] for key, x, score, layer, readout in zip(keys, xs, scores, layers, readouts): r, c = row_index[key], col_index[x] # Several layers can surface the same meta-token at one position; keep # the strongest readout. if np.isnan(z[r, c]) or score > z[r, c]: z[r, c] = score hover[r][c] = ( f"{html_lib.escape(str(key)[:60])}
" f"Position: {x:g}
" f"Readout score: {score:.4f}
" f"Layer: {layer}
" f"Readout method: {readout}
" f"readout score, not a probability delta" ) fig = go.Figure(data=go.Heatmap( z=z, x=[f"{c:g}" for c in col_values], y=[str(v)[:45] for v in row_values], colorscale='Viridis', hoverongaps=False, text=hover, hovertemplate='%{text}', colorbar=dict(title="Readout
score"), )) fig.update_layout( title=f"Latent Workspace Heatmap ({row_key} x position, color = readout score)", xaxis_title="Generation position / event order", yaxis_title=row_key.capitalize(), template="pts", height=max(400, 60 + 22 * len(row_values)), ) return fig def create_event_trace(df: pd.DataFrame, selected_query: str) -> Tuple[str, go.Figure]: """Step-by-step HTML cards + probability progression for PTS events.""" if df is None or df.empty: return "No events found for this query", _empty_fig("No events", 300) work = df.reset_index(drop=True) work = work.assign(_x=_compute_event_x(work)).sort_values('_x') query_text = str(selected_query or "") html_parts = [f"""

Query: {html_lib.escape(query_text[:100])}{'...' if len(query_text) > 100 else ''}

{len(work)} causal reasoning events for this query

"""] curve_x, curve_y = [], [] for _, row in work.iterrows(): gran = _granularity(row) color = _event_color(row) label = html_lib.escape(str(_val(row, 'label', ''))) category = html_lib.escape(str(_val(row, 'category', 'unknown'))) score = _num(_val(row, 'score')) x = float(row['_x']) if gran == 'latent': metric_html = ( f'' f'readout score {score:.3f}' ) extra = ( f'Layer {_val(row, "layer", "n/a")}' f'{_val(row, "readout_method", "n/a")}' f'observational - no probability delta' ) else: delta = _num(_val(row, 'prob_delta')) metric_html = ( f'' f'{"+" if delta > 0 else ""}{delta:.3f} Δ probability' ) extra = ( f'Before: {_num(_val(row, "prob_before")):.3f}' f'After: {_num(_val(row, "prob_after")):.3f}' ) before, after = _val(row, 'prob_before'), _val(row, 'prob_after') if before is not None and after is not None: curve_x.extend([x, x]) curve_y.extend([_num(before), _num(after)]) html_parts.append(f"""
{html_lib.escape(str(_val(row, 'event_type', 'event')))} | {gran} | pos {x:g} | {category} {metric_html}

{label}

{extra}
""") html_parts.append("
") if curve_x: fig = go.Figure() fig.add_trace(go.Scatter( x=curve_x, y=curve_y, mode='lines+markers', name='Success probability', line=dict(color=COLOR_UNKNOWN, width=2), marker=dict(size=8), )) fig.add_hline(y=0.5, line_dash="dash", line_color="gray") fig.update_layout( title="Success Probability Across Emitted Events", xaxis_title="Generation position / event order", yaxis_title="Success Probability", yaxis_range=[0, 1], template="pts", height=300, ) else: fig = _empty_fig( "No emitted events with prob_before/prob_after for this query.
" "Latent events are observational and carry no probability delta.", 300, ) return "\n".join(html_parts), fig def create_probability_space_visualization(df: pd.DataFrame, color_by: str = 'is_positive') -> go.Figure: """Create a probability space visualization for pivotal tokens (prob_before vs prob_after).""" fig = go.Figure() # Color palette for categorical values CATEGORY_COLORS = [ '#A78BFA', '#34D399', '#FB7185', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF', '#FB923C', '#38BDF8', '#A3E635' ] # Determine color column use_colorscale = False if color_by in df.columns: color_col = df[color_by] if color_by == 'is_positive': colors = ['#34D399' if v else '#FB7185' for v in color_col] else: # Convert to list values = color_col.tolist() if hasattr(color_col, 'tolist') else list(color_col) if len(values) > 0: # Check if numeric if isinstance(values[0], (int, float)) and not isinstance(values[0], bool): colors = values use_colorscale = True else: # Categorical - map to colors unique_vals = list(set(values)) color_map = {val: CATEGORY_COLORS[i % len(CATEGORY_COLORS)] for i, val in enumerate(unique_vals)} colors = [color_map[v] for v in values] else: colors = ['#A78BFA'] * len(df) else: colors = ['#A78BFA'] * len(df) # Create hover text hover_texts = [] for _, row in df.iterrows(): # legacy calls it pivot_token, PTS calls it label. token_text = _val(row, 'pivot_token', _val(row, 'label', 'N/A')) text = f"Token: {token_text}
" text += f"Before: {_num(_val(row, 'prob_before')):.3f}
" text += f"After: {_num(_val(row, 'prob_after')):.3f}
" text += f"Delta: {_num(_val(row, 'prob_delta')):+.3f}
" text += f"Query: {str(_val(row, 'query', ''))[:40]}..." hover_texts.append(text) fig.add_trace(go.Scatter( x=df['prob_before'].tolist(), y=df['prob_after'].tolist(), mode='markers', marker=dict( size=8, color=colors, opacity=0.6, colorscale='Viridis' if use_colorscale else None, showscale=use_colorscale ), hovertext=hover_texts, hoverinfo='text', name='Pivotal Tokens' )) # Add diagonal line (no change) fig.add_trace(go.Scatter( x=[0, 1], y=[0, 1], mode='lines', line=dict(dash='dash', color='gray', width=1), name='No Change Line', showlegend=True )) fig.update_layout( title="Probability Space: Before vs After Pivotal Token", xaxis_title="Probability Before Token", yaxis_title="Probability After Token", xaxis=dict(range=[0, 1]), yaxis=dict(range=[0, 1]), template="pts", height=500 ) # Add annotations fig.add_annotation( x=0.2, y=0.8, text="Positive Impact ↑", showarrow=False, font=dict(color="#34D399", size=12) ) fig.add_annotation( x=0.8, y=0.2, text="Negative Impact ↓", showarrow=False, font=dict(color="#FB7185", size=12) ) return fig def create_embedding_visualization(df: pd.DataFrame, color_by: str = 'is_positive') -> go.Figure: """Create UMAP/t-SNE visualization of embeddings or alternative visualization for pivotal tokens.""" if df.empty: fig = go.Figure() fig.add_annotation(text="No data available", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) fig.update_layout(template="pts") return fig dataset_type = detect_dataset_type(df) # Check for embeddings embedding_col = None for col in ['sentence_embedding', 'steering_vector']: if col in df.columns: embedding_col = col break # For pivotal tokens without embeddings, create a probability space visualization if embedding_col is None: if dataset_type == 'pivotal_tokens' and 'prob_before' in df.columns and 'prob_after' in df.columns: return create_probability_space_visualization(df, color_by) # unified events: only emitted events live in probability space. Latent # events have no prob_before/prob_after and must not be plotted there. if dataset_type in PTS_TYPES and 'prob_before' in df.columns and 'prob_after' in df.columns: emitted = df[_granularity_series(df) != 'latent'] emitted = emitted.dropna(subset=['prob_before', 'prob_after']) if emitted.empty: return _empty_fig( "No emitted events with probabilities to plot.
" "Latent meta-token events are observational: they carry a readout " "score, not a probability delta, so they have no place in " "probability space.", 450, ) if color_by not in emitted.columns: color_by = 'is_positive' if 'is_positive' in emitted.columns else 'event_type' return create_probability_space_visualization(emitted.reset_index(drop=True), color_by) fig = go.Figure() fig.add_annotation( text="No embedding data found. Embeddings are available in thought_anchors and steering_vectors datasets.", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False, font=dict(size=12, color="#a0a0a0") ) fig.update_layout(template="pts", height=400) return fig # Extract embeddings embeddings = [] valid_indices = [] for idx, row in df.iterrows(): emb = row.get(embedding_col, []) # Handle both list and numpy array formats if emb is not None: if isinstance(emb, np.ndarray) and len(emb) > 0: embeddings.append(emb.tolist()) valid_indices.append(idx) elif isinstance(emb, list) and len(emb) > 0: embeddings.append(emb) valid_indices.append(idx) if len(embeddings) < 3: fig = go.Figure() fig.add_annotation(text="Not enough embeddings for visualization (need at least 3)", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False) fig.update_layout(template="pts") return fig embeddings = np.array(embeddings) # Reduce dimensionality n_samples = len(embeddings) # t-SNE requires perplexity < n_samples; the clamp keeps small datasets # (a handful of embeddings) from blowing up. perplexity = min(30, max(5, n_samples // 3)) perplexity = max(2, min(perplexity, n_samples - 1)) if embeddings.shape[1] > 50: # First reduce with PCA pca = PCA(n_components=min(50, n_samples - 1)) embeddings = pca.fit_transform(embeddings) # Then t-SNE for visualization tsne = TSNE(n_components=2, perplexity=perplexity, random_state=42) coords = tsne.fit_transform(embeddings) # Create dataframe for plotting plot_df = df.iloc[valid_indices].copy() plot_df['x'] = coords[:, 0].tolist() plot_df['y'] = coords[:, 1].tolist() # Handle color column if color_by not in plot_df.columns: color_by = 'is_positive' if 'is_positive' in plot_df.columns else None fig = go.Figure() # Determine text field for hover (legacy: sentence/pivot_token; PTS: label) if 'sentence' in plot_df.columns: text_field = 'sentence' elif 'pivot_token' in plot_df.columns: text_field = 'pivot_token' else: text_field = 'label' if color_by and color_by in plot_df.columns: # Group by color column for separate traces if color_by == 'is_positive': # Special handling for boolean is_positive for is_pos in [True, False]: mask = plot_df[color_by] == is_pos subset = plot_df[mask] if len(subset) > 0: hover_texts = [str(row.get(text_field, ''))[:100] for _, row in subset.iterrows()] fig.add_trace(go.Scatter( x=subset['x'].tolist(), y=subset['y'].tolist(), mode='markers', name='Positive' if is_pos else 'Negative', marker=dict( size=8, color='#34D399' if is_pos else '#FB7185', opacity=0.7 ), hovertext=hover_texts, hoverinfo='text' )) else: # Categorical coloring unique_vals = plot_df[color_by].unique() colors = ['#A78BFA', '#34D399', '#FB7185', '#FBBF24', '#A78BFA', '#F472B6', '#2DD4BF', '#FB923C', '#38BDF8', '#A3E635'] for i, val in enumerate(unique_vals): mask = plot_df[color_by] == val subset = plot_df[mask] if len(subset) > 0: hover_texts = [str(row.get(text_field, ''))[:100] for _, row in subset.iterrows()] fig.add_trace(go.Scatter( x=subset['x'].tolist(), y=subset['y'].tolist(), mode='markers', name=str(val), marker=dict( size=8, color=colors[i % len(colors)], opacity=0.7 ), hovertext=hover_texts, hoverinfo='text' )) else: # No color grouping hover_texts = [str(row.get(text_field, ''))[:100] for _, row in plot_df.iterrows()] fig.add_trace(go.Scatter( x=plot_df['x'].tolist(), y=plot_df['y'].tolist(), mode='markers', name='Embeddings', marker=dict( size=8, color='#A78BFA', opacity=0.7 ), hovertext=hover_texts, hoverinfo='text' )) fig.update_layout( title="Embedding Space Visualization (t-SNE)", xaxis_title="t-SNE 1", yaxis_title="t-SNE 2", template="pts", height=500, showlegend=True ) return fig def create_pivotal_token_trace(df: pd.DataFrame, selected_query: str) -> Tuple[str, go.Figure]: """Create a trace visualization for pivotal tokens in a query.""" if df.empty: return "No tokens found for this query", go.Figure() # Build HTML for token cards html_parts = [f"""

Query: {selected_query[:100]}{'...' if len(selected_query) > 100 else ''}

Found {len(df)} pivotal tokens for this query

"""] prob_deltas = [] token_indices = [] for idx, (_, row) in enumerate(df.iterrows()): token = row.get('pivot_token', 'N/A') context = row.get('pivot_context', '') is_positive = row.get('is_positive', row.get('prob_delta', 0) > 0) prob_delta = row.get('prob_delta', 0) prob_before = row.get('prob_before', 0) prob_after = row.get('prob_after', 0) task_type = row.get('task_type', 'unknown') # Color based on impact bg_color = "rgba(34, 197, 94, 0.2)" if is_positive else "rgba(239, 68, 68, 0.2)" border_color = "#34D399" if is_positive else "#FB7185" # Show full context in a scrollable container - no truncation # Escape HTML characters in context and token context_escaped = html_lib.escape(str(context)) token_escaped = html_lib.escape(str(token)) # Build token card with full context (scrollable) card_html = f"""
Token #{idx + 1} | {task_type} {'+'if prob_delta > 0 else ''}{prob_delta:.3f}
{context_escaped}{token_escaped}
Before: {prob_before:.3f} After: {prob_after:.3f} Context: {len(context)} chars
""" html_parts.append(card_html) prob_deltas.append(prob_delta) token_indices.append(idx) html_parts.append("
") # Create probability delta chart fig = go.Figure() # Ensure all values are Python native types prob_deltas = [float(d) for d in prob_deltas] colors = ['#34D399' if d > 0 else '#FB7185' for d in prob_deltas] fig.add_trace(go.Bar( x=token_indices, y=prob_deltas, marker_color=colors, name='Probability Delta', hovertemplate='Token #%{x}
Δ Prob: %{y:.3f}' )) fig.add_hline(y=0, line_dash="dash", line_color="gray") fig.update_layout( title="Probability Impact per Token", xaxis_title="Token Index", yaxis_title="Probability Delta", template="pts", height=300 ) return "\n".join(html_parts), fig def create_circuit_visualization(df: pd.DataFrame, query_idx: int = 0) -> Tuple[str, go.Figure]: """Create step-by-step circuit visualization for reasoning trace.""" if df.empty: return "No data available", go.Figure() dataset_type = detect_dataset_type(df) # Get unique queries queries = df['query'].unique() if 'query' in df.columns else [] if len(queries) == 0: return "No queries found", go.Figure() query_idx = min(query_idx, len(queries) - 1) selected_query = queries[query_idx] # Filter to this query query_df = df[df['query'] == selected_query].copy() # PTS unified events if dataset_type in PTS_TYPES: return create_event_trace(query_df, selected_query) # For pivotal tokens and steering vectors, use the token trace visualization if dataset_type in ('pivotal_tokens', 'steering_vectors'): return create_pivotal_token_trace(query_df, selected_query) # Sort by sentence_id if available, otherwise keep original order if 'sentence_id' in query_df.columns: query_df = query_df.sort_values('sentence_id') else: query_df = query_df.reset_index(drop=True) # Build HTML for step-by-step view html_parts = [f"""

Query: {selected_query[:100]}{'...' if len(selected_query) > 100 else ''}

"""] prob_values = [] sentence_ids = [] for idx, row in query_df.iterrows(): sentence = row.get('sentence', 'N/A') sentence_id = row.get('sentence_id', idx) is_positive = row.get('is_positive', row.get('prob_delta', 0) > 0) prob_delta = row.get('prob_delta', 0) category = row.get('sentence_category', 'unknown') importance = row.get('importance_score', abs(prob_delta)) # Verification info verification_score = row.get('verification_score', None) arithmetic_errors = row.get('arithmetic_errors', []) # Color based on impact bg_color = "rgba(34, 197, 94, 0.2)" if is_positive else "rgba(239, 68, 68, 0.2)" border_color = "#34D399" if is_positive else "#FB7185" # Build step card step_html = f"""
Step {sentence_id} | {category} {'+'if prob_delta > 0 else ''}{prob_delta:.3f}

{sentence}

Importance: {importance:.3f} """ if verification_score is not None: v_color = "#34D399" if verification_score > 0.5 else "#FB7185" step_html += f""" Verification: {verification_score:.2f} """ if arithmetic_errors: step_html += """ Has Errors """ step_html += """
""" html_parts.append(step_html) prob_values.append(row.get('prob_with_sentence', 0.5)) sentence_ids.append(sentence_id) html_parts.append("
") # Create probability progression chart fig = go.Figure() colors = ['#34D399' if p > 0.5 else '#FB7185' for p in prob_values] fig.add_trace(go.Scatter( x=[int(s) if isinstance(s, (int, np.integer)) else s for s in sentence_ids], y=[float(p) for p in prob_values], mode='lines+markers', name='Success Probability', line=dict(color='#A78BFA', width=2), marker=dict(size=10, color=colors) )) fig.add_hline(y=0.5, line_dash="dash", line_color="gray", annotation_text="50% threshold") fig.update_layout( title="Probability Progression Through Reasoning", xaxis_title="Sentence ID", yaxis_title="Success Probability", yaxis_range=[0, 1], template="pts", height=300 ) return "\n".join(html_parts), fig def create_statistics_dashboard(df: pd.DataFrame) -> Tuple[str, go.Figure]: """Create statistics dashboard for the dataset.""" if df.empty: return "No data available", go.Figure() dataset_type = detect_dataset_type(df) is_pts = dataset_type in PTS_TYPES # Build statistics stats = { "Total Items": len(df), "Dataset Type": dataset_type, } if is_pts: gran = _granularity_series(df) stats["Latent Events"] = int((gran == 'latent').sum()) stats["Token Events"] = int((gran == 'token').sum()) stats["Sentence Events"] = int((gran == 'sentence').sum()) link_count = 0 for col in ('linked_event_ids', 'precedes_event_ids'): if col in df.columns: link_count += int(sum(len(_as_list(v)) for v in df[col])) stats["Event Links"] = link_count if 'query' in df.columns: stats["Queries"] = int(df['query'].nunique()) if 'category' in df.columns: stats["Categories"] = int(df['category'].dropna().nunique()) if 'layer' in df.columns and df['layer'].notna().any(): stats["Layer Range"] = ( f"{int(df['layer'].min())}-{int(df['layer'].max())}" ) if 'readout_method' in df.columns and df['readout_method'].notna().any(): methods = sorted({str(m) for m in df['readout_method'].dropna().unique()}) stats["Readout"] = ", ".join(methods) if 'is_positive' in df.columns: # Latent events are null here by design; count them separately rather # than folding them into "negative". positive_count = int( df['is_positive'].map(lambda v: bool(v) if v is not None and v is not pd.NA and v == v else False).sum() ) unscored = int(df['is_positive'].isna().sum()) stats["Positive Items"] = positive_count stats["Negative Items"] = int(len(df) - positive_count - unscored) if unscored: stats["Unscored (latent)"] = unscored if 'prob_delta' in df.columns and df['prob_delta'].notna().any(): stats["Avg Prob Delta"] = f"{df['prob_delta'].mean():.3f}" stats["Max Prob Delta"] = f"{df['prob_delta'].max():.3f}" if 'importance_score' in df.columns: stats["Avg Importance"] = f"{df['importance_score'].mean():.3f}" if 'sentence_category' in df.columns: category_counts = df['sentence_category'].value_counts() stats["Categories"] = len(category_counts) if 'model_id' in df.columns: stats["Models"] = df['model_id'].nunique() # Build HTML. Tiles are tinted by the channel they report on, so the three # scales stay visually distinct even in a plain count. TILE_CHANNEL = { 'Latent Events': C_LATENT, 'Token Events': C_TOKEN, 'Sentence Events': C_SENTENCE, 'Positive Items': C_POSITIVE, 'Negative Items': C_NEGATIVE, 'Causal Links': C_LATENT, } html_parts = ['
'] for key, value in stats.items(): c = TILE_CHANNEL.get(key) accent = ' accent' if c else '' style = f' style="--c:{c}"' if c else '' html_parts.append( f'
' f'
{html_lib.escape(str(key))}
' f'
{html_lib.escape(str(value))}
' f'
' ) html_parts.append('
') # causal-event datasets get their own panel set: emitted deltas and latent readout scores are # different quantities and are never binned into the same histogram. if is_pts: fig = make_subplots( rows=1, cols=3, subplot_titles=( "Δ probability (emitted events)", "Events by type", "Readout score (latent events)", ), ) gran = _granularity_series(df) emitted = df[gran != 'latent'] latent = df[gran == 'latent'] if 'prob_delta' in emitted.columns and emitted['prob_delta'].notna().any(): data = emitted['prob_delta'].dropna().values counts, bin_edges = np.histogram(data, bins=30) centers = [(bin_edges[i] + bin_edges[i + 1]) / 2 for i in range(len(bin_edges) - 1)] fig.add_trace( go.Bar(x=centers, y=counts.tolist(), name="Δ probability", marker_color=COLOR_UNKNOWN, width=(bin_edges[1] - bin_edges[0]) * 0.9), row=1, col=1, ) if 'event_type' in df.columns: type_counts = df['event_type'].value_counts() colors = [ COLOR_LATENT if t == EVENT_LATENT else COLOR_POSITIVE for t in type_counts.index.tolist() ] fig.add_trace( go.Bar(x=type_counts.index.tolist(), y=type_counts.values.tolist(), name="Events", marker_color=colors), row=1, col=2, ) if not latent.empty and 'score' in latent.columns and latent['score'].notna().any(): data = latent['score'].dropna().values counts, bin_edges = np.histogram(data, bins=20) centers = [(bin_edges[i] + bin_edges[i + 1]) / 2 for i in range(len(bin_edges) - 1)] fig.add_trace( go.Bar(x=centers, y=counts.tolist(), name="Readout score", marker_color=COLOR_LATENT, width=(bin_edges[1] - bin_edges[0]) * 0.9), row=1, col=3, ) else: fig.add_annotation( text="no latent events", xref="x3 domain", yref="y3 domain", x=0.5, y=0.5, showarrow=False, font=dict(size=11, color="#6b7280"), row=1, col=3, ) fig.update_xaxes(title_text="Δ probability", row=1, col=1) fig.update_xaxes(title_text="Readout score (not a Δ probability)", row=1, col=3) fig.update_layout(template="pts", height=380, showlegend=False) return "\n".join(html_parts), fig # Determine what to show in second chart second_chart_title = "Category Distribution" if 'sentence_category' in df.columns: second_chart_title = "Sentence Category" elif 'reasoning_pattern' in df.columns: second_chart_title = "Reasoning Pattern" elif 'task_type' in df.columns: second_chart_title = "Task Type" elif 'is_positive' in df.columns: second_chart_title = "Positive vs Negative" # Create distribution charts fig = make_subplots(rows=1, cols=2, subplot_titles=("Probability Delta Distribution", second_chart_title)) # First chart: Probability Delta histogram (using numpy for binning) if 'prob_delta' in df.columns and len(df['prob_delta'].dropna()) > 0: prob_data = df['prob_delta'].dropna().values # Create histogram manually using numpy counts, bin_edges = np.histogram(prob_data, bins=30) bin_centers = [(bin_edges[i] + bin_edges[i+1]) / 2 for i in range(len(bin_edges)-1)] fig.add_trace( go.Bar(x=bin_centers, y=counts.tolist(), name="Prob Delta", marker_color='#A78BFA', width=(bin_edges[1]-bin_edges[0])*0.9), row=1, col=1 ) elif 'prob_after' in df.columns and len(df['prob_after'].dropna()) > 0: # Fallback: show prob_after distribution prob_data = df['prob_after'].dropna().values counts, bin_edges = np.histogram(prob_data, bins=30) bin_centers = [(bin_edges[i] + bin_edges[i+1]) / 2 for i in range(len(bin_edges)-1)] fig.add_trace( go.Bar(x=bin_centers, y=counts.tolist(), name="Prob After", marker_color='#A78BFA', width=(bin_edges[1]-bin_edges[0])*0.9), row=1, col=1 ) # Second chart: Categories, patterns, or task types if 'sentence_category' in df.columns: category_counts = df['sentence_category'].value_counts() fig.add_trace( go.Bar(x=category_counts.index.tolist(), y=category_counts.values.tolist(), name="Categories", marker_color='#34D399'), row=1, col=2 ) elif 'reasoning_pattern' in df.columns: pattern_counts = df['reasoning_pattern'].value_counts() fig.add_trace( go.Bar(x=pattern_counts.index.tolist(), y=pattern_counts.values.tolist(), name="Patterns", marker_color='#34D399'), row=1, col=2 ) elif 'task_type' in df.columns: task_counts = df['task_type'].value_counts() fig.add_trace( go.Bar(x=task_counts.index.tolist(), y=task_counts.values.tolist(), name="Task Types", marker_color='#34D399'), row=1, col=2 ) elif 'is_positive' in df.columns: pos_neg_counts = df['is_positive'].value_counts() labels = ['Positive' if v else 'Negative' for v in pos_neg_counts.index.tolist()] fig.add_trace( go.Bar(x=labels, y=pos_neg_counts.values.tolist(), name="Impact", marker_color=['#34D399' if l == 'Positive' else '#FB7185' for l in labels]), row=1, col=2 ) fig.update_layout( template="pts", height=350, showlegend=False ) return "\n".join(html_parts), fig # ============================================================================ # Gradio Interface # ============================================================================ # Global state for loaded data. "filtered" holds the Event Explorer's current # selection so the detail slider indexes into what the user is actually looking at. current_data = {"df": pd.DataFrame(), "type": "unknown", "filtered": pd.DataFrame()} DPO_NOTICE_HTML = """

DPO Pairs Dataset

This visualization is not available for DPO pairs datasets.

DPO pairs contain prompt/chosen/rejected structure without token-level context.

Try loading a causal_events, pivotal_tokens or thought_anchors dataset instead.

""" def _query_choices(df: pd.DataFrame) -> List[str]: """Truncated dropdown labels for each unique query.""" if df.empty or 'query' not in df.columns: return [] choices = [] for i, q in enumerate(df['query'].unique().tolist()): q_str = str(q) if q is not None else "" if len(q_str) > 80: choices.append(f"[{i+1}] {q_str[:77]}...") else: choices.append(f"[{i+1}] {q_str}") return choices def load_dataset_action(source_type: str, dataset_id: str, file_upload): """Handle dataset loading and return all visualization updates.""" global current_data def blank(message: str): empty_fig = go.Figure() empty_fig.update_layout(template="pts") return (message, "", "No data", empty_fig, empty_fig, empty_fig, "No data", empty_fig, empty_fig, empty_fig, gr.update(maximum=0, value=0), gr.update(choices=[], value=None), gr.update(choices=["All"], value="All"), gr.update(choices=["All"], value="All"), "No data", empty_fig, "No data", empty_fig) if source_type == "HuggingFace Hub": if not dataset_id: return blank("Please enter a dataset ID") df, msg = load_hf_dataset(dataset_id) else: # Local File if file_upload is None: return blank("Please upload a file") df, msg = load_jsonl_file(file_upload.name) if df.empty: return blank(msg) current_data["df"] = df current_data["type"] = detect_dataset_type(df) current_data["filtered"] = df columns_info = f"Columns: {', '.join(df.columns[:10])}" if len(df.columns) > 10: columns_info += f" ... and {len(df.columns) - 10} more" # Render only what is visible on load -- the Overview (and the cheap Event # Explorer below). The graph, embedding, and timeline tabs each render six # Plotly figures whose serialization + browser paint dominate load on a weak # Space CPU (~90s for all six). They are computed on demand when their tab is # opened (see the tab.select handlers), which keeps load to a couple of # seconds. The Python for each is <0.5s; the cost is figure transfer/paint. stats_html, stats_fig = create_statistics_dashboard(df) lazy = _empty_fig("Open this tab to render.", 480) graph_fig = lazy embed_fig = lazy circuit_html = "
Open this tab to render.
" circuit_fig = lazy timeline_fig = lazy heatmap_fig = lazy # Event Explorer filter choices come from the data itself. type_choices = ["All"] if 'event_type' in df.columns: type_choices += sorted({str(v) for v in df['event_type'].dropna().unique()}) category_choices = ["All"] for col in ('category', 'sentence_category'): if col in df.columns: category_choices += sorted({str(v) for v in df[col].dropna().unique()}) break ev_summary, ev_fig = describe_event_selection(df) detail_html, detail_fig = get_event_details(0) return (msg, f"Dataset type: {current_data['type']}\n{columns_info}", stats_html, stats_fig, graph_fig, embed_fig, circuit_html, circuit_fig, timeline_fig, heatmap_fig, gr.update(maximum=max(0, len(df) - 1), value=0), gr.update(choices=_query_choices(df), value=None), gr.update(choices=type_choices, value="All"), gr.update(choices=category_choices, value="All"), ev_summary, ev_fig, detail_html, detail_fig) def create_readout_score_chart(row) -> go.Figure: """Detail chart for a latent event. Deliberately not a before/after probability chart: a latent event has no prob_before or prob_after. Showing one would assert a causal effect that was never measured. """ score = _num(_val(row, 'score')) fig = go.Figure() fig.add_trace(go.Bar( x=['Readout score'], y=[score], marker_color=COLOR_LATENT, text=[f"{score:.4f}"], textposition='outside', )) fig.update_layout( title=( f"Latent readout score (layer {_val(row, 'layer', 'n/a')}, " f"{_val(row, 'readout_method', 'n/a')}) - not a probability delta" ), yaxis_title="Readout score", yaxis_range=[0, max(1.0, score * 1.2)], template="pts", height=300, ) return fig def create_latent_detail_html(row) -> str: """HTML card for a latent meta-token event.""" context = html_lib.escape(str(_val(row, 'context', ''))) label = html_lib.escape(str(_val(row, 'label', ''))) score = _num(_val(row, 'score')) return f"""
Latent meta-token | layer {_val(row, 'layer', 'n/a')} | readout {_val(row, 'readout_method', 'n/a')} | offset {_val(row, 'position', 'n/a')} | category {html_lib.escape(str(_val(row, 'category', 'unknown')))} Readout score: {score:.4f}
{context}{label}

This event is observational: it was read out of the model's residual stream, not intervened on. Its score is a readout score, not a probability delta, and is not comparable to the scores on emitted token and sentence events.

""" def get_event_details(idx: int) -> Tuple[str, go.Figure]: """Detail view for one event from the current Event Explorer selection.""" df = current_data.get("filtered") if df is None or df.empty: df = current_data["df"] dataset_type = current_data.get("type", "unknown") if df is None or df.empty: return "No data available. Please load a dataset first.", go.Figure() if dataset_type == 'dpo_pairs': return DPO_NOTICE_HTML, go.Figure() try: idx = int(idx) except (TypeError, ValueError): idx = 0 if idx >= len(df) or idx < 0: return "Index out of range", go.Figure() row = df.iloc[idx] # latent events get their own card: no before/after probability exists. if is_pts_events(df) and _granularity(row) == 'latent': return create_latent_detail_html(row), create_readout_score_chart(row) # legacy pivot_context/prefix_context, PTS context. Same for the label. context = _val(row, 'pivot_context', _val(row, 'prefix_context', _val(row, 'context', ''))) token = _val(row, 'pivot_token', _val(row, 'sentence', _val(row, 'label', ''))) prob_delta = _num(_val(row, 'prob_delta')) prob_before = _num(_val(row, 'prob_before', _val(row, 'prob_without_sentence', 0.5)), 0.5) prob_after = _num(_val(row, 'prob_after', _val(row, 'prob_with_sentence', 0.5)), 0.5) if not context and not token: html = """

Missing Data

This dataset doesn't have the expected fields for token visualization.

""" return html, go.Figure() html = create_token_highlight_html(context, token, prob_delta) chart = create_probability_chart(prob_before, prob_after) return html, chart # Kept for backwards compatibility with anything importing the old name. get_token_details = get_event_details def _row_score(row) -> float: """The score used by the Event Explorer's min-score filter. PTS events carry ``score`` directly. legacy records get |prob_delta| (or the thought-anchor importance score), which is the same quantity PTS stores. """ score = _val(row, 'score') if score is not None: return _num(score) importance = _val(row, 'importance_score') if importance is not None: return _num(importance) return abs(_num(_val(row, 'prob_delta'))) def describe_event_selection(df: pd.DataFrame) -> Tuple[str, go.Figure]: """Summary cards + score distribution for the Event Explorer's selection.""" if df is None or df.empty: return ( '
No events match these filters.
', _empty_fig("No events match these filters", 320), ) is_pts = is_pts_events(df) if is_pts: gran = _granularity_series(df) counts = { 'latent': int((gran == 'latent').sum()), 'token': int((gran == 'token').sum()), 'sentence': int((gran == 'sentence').sum()), } else: gran = pd.Series(['token'] * len(df), index=df.index) counts = {'latent': 0, 'token': len(df), 'sentence': 0} cards = [ ("Events", len(df)), ("Latent", counts['latent']), ("Token", counts['token']), ("Sentence", counts['sentence']), ] html_parts = ['
'] for name, value in cards: html_parts.append(f"""
{value}
{name}
""") html_parts.append('
') fig = go.Figure() # Vectorized: iterrows over 14k events took seconds. Emitted score is # |prob_delta| (fall back to |score|); latent score is the readout score. # Every series is aligned to df's index so masks never mismatch on datasets # that lack a `score`/`prob_delta` column (e.g. steering vectors). nan = pd.Series([float('nan')] * len(df), index=df.index) score_col = pd.to_numeric(df['score'], errors='coerce') if 'score' in df.columns else nan delta = pd.to_numeric(df['prob_delta'], errors='coerce').abs() if 'prob_delta' in df.columns else nan emitted_all = delta.where(delta.notna(), score_col.abs()) is_latent = pd.Series(gran.values == 'latent', index=df.index) emitted_scores = emitted_all[~is_latent].dropna().tolist() latent_scores = score_col[is_latent].dropna().tolist() # Two separate traces, never one merged histogram: |Δ probability| and # readout score are different quantities on different scales. if emitted_scores: fig.add_trace(go.Histogram( x=emitted_scores, name="Emitted |Δ probability|", marker_color=COLOR_POSITIVE, opacity=0.7, nbinsx=30, )) if latent_scores: fig.add_trace(go.Histogram( x=latent_scores, name="Latent readout score", marker_color=COLOR_LATENT, opacity=0.7, nbinsx=30, )) fig.update_layout( title="Score distribution of the current selection", xaxis_title="Score (|Δ probability| for emitted events, readout score for latent events)", yaxis_title="Events", barmode='overlay', template="pts", height=320, ) return "\n".join(html_parts), fig def apply_event_filters(event_type: str, granularity: str, polarity: str, category: str, min_score: float, layer_min: float, layer_max: float): """Filter the loaded events and refresh the Event Explorer.""" df = current_data["df"] if df is None or df.empty: current_data["filtered"] = pd.DataFrame() summary, fig = describe_event_selection(pd.DataFrame()) return (summary, fig, gr.update(maximum=0, value=0), "No data available. Please load a dataset first.", go.Figure()) if current_data.get("type") == 'dpo_pairs': current_data["filtered"] = df summary, fig = describe_event_selection(pd.DataFrame()) return (summary, fig, gr.update(maximum=0, value=0), DPO_NOTICE_HTML, go.Figure()) work = df.copy() is_pts = is_pts_events(work) if is_pts and event_type and event_type != "All" and 'event_type' in work.columns: work = work[work['event_type'].astype(str) == event_type] if is_pts and granularity and granularity != "All" and not work.empty: work = work[_granularity_series(work) == granularity] if polarity and polarity != "All" and not work.empty: def valence(row): """True / False / None -- latent events are always None.""" if _granularity(row) == 'latent': return None is_positive = _val(row, 'is_positive') if is_positive is not None: return bool(is_positive) delta = _val(row, 'prob_delta') if delta is None: return None return _num(delta) > 0 positives = work.apply(valence, axis=1) if polarity == "Positive": work = work[positives.apply(lambda v: v is True)] elif polarity == "Negative": work = work[positives.apply(lambda v: v is False)] elif polarity.startswith("Unscored"): work = work[positives.apply(lambda v: v is None)] if category and category != "All" and not work.empty: cat_col = 'category' if 'category' in work.columns else ( 'sentence_category' if 'sentence_category' in work.columns else None ) if cat_col: work = work[work[cat_col].astype(str) == category] if not work.empty and min_score: # Apply the threshold only to emitted events, whose score is a # probability delta. A latent event's score is a readout probability on # a different scale entirely -- one slider across both would drop a # pivotal token worth +0.45 while keeping a banal " the" readout at 0.92. def score_ok(row) -> bool: if _granularity(row) == 'latent': return True return _row_score(row) >= float(min_score) work = work[work.apply(score_ok, axis=1)] # Layer range only constrains latent events; emitted events have no layer. if not work.empty and 'layer' in work.columns: lo, hi = float(layer_min), float(layer_max) if lo > hi: lo, hi = hi, lo def layer_ok(row) -> bool: if _granularity(row) != 'latent': return True layer = _val(row, 'layer') if layer is None: return True return lo <= _num(layer) <= hi work = work[work.apply(layer_ok, axis=1)] work = work.reset_index(drop=True) current_data["filtered"] = work summary, fig = describe_event_selection(work) detail_html, detail_fig = get_event_details(0) return (summary, fig, gr.update(maximum=max(0, len(work) - 1), value=0), detail_html, detail_fig) def get_original_query_from_label(label: str) -> str: """Extract original query from truncated dropdown label like '[1] query...'""" if not label or not isinstance(label, str): return None df = current_data["df"] if df.empty or 'query' not in df.columns: return None # Extract index from "[N] query..." format match = re.match(r'\[(\d+)\]', label) if match: idx = int(match.group(1)) - 1 # Convert to 0-based index queries = df['query'].unique().tolist() if 0 <= idx < len(queries): return queries[idx] return None def update_graph_visualization(query_dropdown: str = None): """Update the causal event graph (falls back to the v1 reasoning graph).""" dataset_type = current_data.get("type", "unknown") if dataset_type == 'dpo_pairs': return _empty_fig( "Causal Event Graph is not available for DPO pairs datasets.
" "Load a causal_events, pivotal_tokens or thought_anchors dataset.", ) # Convert truncated label back to original query original_query = get_original_query_from_label(query_dropdown) return create_causal_event_graph(current_data["df"], original_query) def update_embedding_visualization(color_by: str): """Update the embedding visualization.""" dataset_type = current_data.get("type", "unknown") if dataset_type == 'dpo_pairs': return _empty_fig( "Embedding Space is not available for DPO pairs datasets.
" "Load a pivotal_tokens, thought_anchors, or steering_vectors dataset.", ) return create_embedding_visualization(current_data["df"], color_by) def _query_at_index(df: pd.DataFrame, query_idx: int) -> Optional[str]: if df.empty or 'query' not in df.columns: return None queries = df['query'].unique().tolist() if not queries: return None idx = max(0, min(int(query_idx), len(queries) - 1)) return queries[idx] def update_circuit_view(query_idx: int): """Update the Reasoning Timeline tab (timeline + heatmap + trace).""" dataset_type = current_data.get("type", "unknown") df = current_data["df"] if dataset_type == 'dpo_pairs': html = """

DPO Pairs Dataset

The reasoning timeline is not available for DPO pairs datasets.

Load a causal_events, pivotal_tokens or thought_anchors dataset to explore reasoning circuits.

""" empty = _empty_fig("Not available for DPO pairs datasets") return html, go.Figure(), empty, empty try: query_idx = int(query_idx) except (TypeError, ValueError): query_idx = 0 selected_query = _query_at_index(df, query_idx) circuit_html, circuit_fig = create_circuit_visualization(df, query_idx) timeline_fig = create_reasoning_timeline(df, selected_query) heatmap_fig = create_workspace_heatmap(df, selected_query) return circuit_html, circuit_fig, timeline_fig, heatmap_fig def update_statistics(): """Update the statistics dashboard.""" return create_statistics_dashboard(current_data["df"]) def get_query_list(): """Get list of unique queries with truncated display labels.""" df = current_data["df"] if df.empty or 'query' not in df.columns: return gr.update(choices=[], value=None) return gr.update(choices=_query_choices(df), value=None) def refresh_all(): """Refresh all visualizations.""" df = current_data["df"] if df.empty: empty_fig = go.Figure() empty_fig.update_layout(template="pts") return ( "No data loaded", empty_fig, empty_fig, empty_fig, "No data loaded", empty_fig, empty_fig, empty_fig, ) stats_html, stats_fig = create_statistics_dashboard(df) graph_fig = create_causal_event_graph(df) embed_fig = create_embedding_visualization(df) circuit_html, circuit_fig = create_circuit_visualization(df) first_query = _query_at_index(df, 0) timeline_fig = create_reasoning_timeline(df, first_query) heatmap_fig = create_workspace_heatmap(df, first_query) return (stats_html, stats_fig, graph_fig, embed_fig, circuit_html, circuit_fig, timeline_fig, heatmap_fig) # ============================================================================ # Build Gradio App # ============================================================================ # Pre-defined HuggingFace datasets. # The legacy datasets below still load and render exactly as they did before; the # causal-event datasets use the unified CausalReasoningEvent schema. # Only datasets that actually exist on the Hub. The visualizer reads PTS # causal-event files too -- upload one and add it here, or load it from disk with # the file upload. Listing PTS datasets that have not been published yet would put # entries in the dropdown that can only fail. # The unified `-pts` datasets are what this visualizer is for: pivotal reasoning # events at all three scales, with the multiscale timeline, causal graph, and # workspace heatmap. The other PTS repos don't earn a slot here: # - `-thought-anchors` duplicate the sentence events already in `-pts` # - `-dpo-pairs` are training pairs, not events (no charts render) # - `-steering-vectors` are activation vectors -- only the embedding tab # applies; the multiscale views are token-only # All of them remain linked in this Space's README `datasets:` list for # discovery. Any of them can still be loaded by pasting its id or uploading it. HF_DATASETS = [ "codelion/Qwen3-0.6B-pts", "codelion/DeepSeek-R1-Distill-Qwen-1.5B-pts", ] DEFAULT_DATASET = "codelion/Qwen3-0.6B-pts" # ============================================================================ # Design system # # PTS reads signals out of a model's internals at three depths. The interface is # built as an *instrument* for that -- a spectrometer readout, not an admin # dashboard. Three scales, three channels, each with its own signal colour; that # colour system is the identity and it is used with discipline everywhere: # charts, panel accents, badges, legends. # # LATENT violet hidden, deep in the residual stream # TOKEN sky emitted, sharp, a single decision point # SENTENCE amber emitted, extended over a reasoning step # # Valence (green/rose) is deliberately a SEPARATE axis from channel, because # latent events have no valence -- they are never green or red, and the palette # must make that impossible to render by accident. # ============================================================================ # (palette defined at the top of this file) # One Plotly template for every figure in the app. Registered as "pts" and # swapped in for plotly_dark throughout, so no chart can drift off-system. _pts_template = go.layout.Template() _pts_template.layout = go.Layout( paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", font=dict(family="'IBM Plex Sans', system-ui, sans-serif", size=12, color=C_TEXT), title=dict( font=dict(family="'IBM Plex Mono', monospace", size=14, color=C_TEXT), x=0.01, xanchor="left", pad=dict(l=4, t=8, b=8), ), colorway=[C_LATENT, C_TOKEN, C_SENTENCE, C_POSITIVE, C_NEGATIVE, "#F472B6", "#2DD4BF"], xaxis=dict( gridcolor=C_LINE_SOFT, zerolinecolor=C_LINE, linecolor=C_LINE, tickfont=dict(family="'IBM Plex Mono', monospace", size=10, color=C_TEXT_MUTED), title=dict(font=dict(size=11, color=C_TEXT_MUTED)), showline=True, ticks="outside", tickcolor=C_LINE, ticklen=4, ), yaxis=dict( gridcolor=C_LINE_SOFT, zerolinecolor=C_LINE, linecolor=C_LINE, tickfont=dict(family="'IBM Plex Mono', monospace", size=10, color=C_TEXT_MUTED), title=dict(font=dict(size=11, color=C_TEXT_MUTED)), showline=True, ticks="outside", tickcolor=C_LINE, ticklen=4, ), legend=dict( bgcolor="rgba(17,23,34,0.85)", bordercolor=C_LINE, borderwidth=1, font=dict(family="'IBM Plex Mono', monospace", size=10, color=C_TEXT_MUTED), ), hoverlabel=dict( bgcolor=C_PANEL_HI, bordercolor=C_LINE, font=dict(family="'IBM Plex Mono', monospace", size=11, color=C_TEXT), align="left", ), margin=dict(l=56, r=24, t=48, b=48), colorscale=dict(sequential=[[0, "#161E2C"], [0.5, "#5B4BC4"], [1, C_LATENT]]), ) pio.templates["pts"] = _pts_template PTS_THEME = gr.themes.Base( primary_hue=gr.themes.colors.violet, secondary_hue=gr.themes.colors.sky, neutral_hue=gr.themes.colors.slate, font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"], radius_size=gr.themes.sizes.radius_sm, text_size=gr.themes.sizes.text_md, ).set( body_background_fill=C_CANVAS, body_text_color=C_TEXT, background_fill_primary=C_PANEL, background_fill_secondary=C_PANEL_HI, border_color_primary=C_LINE, block_background_fill=C_PANEL, block_border_color=C_LINE, block_label_background_fill="transparent", block_label_text_color=C_TEXT_MUTED, block_title_text_color=C_TEXT, input_background_fill=C_CANVAS, input_border_color=C_LINE, button_primary_background_fill=C_LATENT, button_primary_background_fill_hover="#B9A3FB", button_primary_text_color="#0A0D13", button_secondary_background_fill=C_PANEL_HI, button_secondary_border_color=C_LINE, button_secondary_text_color=C_TEXT, panel_background_fill=C_PANEL, ) CSS = f""" /* ---- canvas: a faint graticule, like an instrument screen ---- */ .gradio-container {{ max-width: 1580px !important; background: linear-gradient(to right, {C_LINE_SOFT}22 1px, transparent 1px) 0 0 / 32px 32px, linear-gradient(to bottom, {C_LINE_SOFT}22 1px, transparent 1px) 0 0 / 32px 32px, radial-gradient(ellipse 90% 55% at 50% -12%, #241C4E55 0%, transparent 70%), {C_CANVAS} !important; }} footer {{ display: none !important; }} /* ---- masthead ---- */ .pts-masthead {{ border: 1px solid {C_LINE}; border-radius: 4px; background: linear-gradient(160deg, {C_PANEL_HI} 0%, {C_PANEL} 60%); padding: 26px 30px 22px; margin-bottom: 18px; position: relative; overflow: hidden; }} /* the three channels, drawn as a signal bar across the top edge */ .pts-masthead::before {{ content: ""; position: absolute; inset: 0 0 auto 0; height: 2px; background: linear-gradient(90deg, {C_LATENT} 0%, {C_LATENT} 33%, {C_TOKEN} 33%, {C_TOKEN} 66%, {C_SENTENCE} 66%, {C_SENTENCE} 100%); }} .pts-wordmark {{ font-family: 'IBM Plex Mono', monospace; font-size: 30px; font-weight: 600; letter-spacing: -0.02em; color: {C_TEXT}; margin: 0 0 2px; }} .pts-wordmark .tag {{ font-size: 11px; font-weight: 500; letter-spacing: 0.16em; color: {C_LATENT}; border: 1px solid {C_LATENT}44; border-radius: 3px; padding: 3px 7px; margin-left: 12px; vertical-align: middle; background: {C_LATENT}12; }} .pts-tagline {{ font-size: 13.5px; color: {C_TEXT_MUTED}; margin: 0 0 20px; max-width: 78ch; line-height: 1.6; }} .pts-tagline b {{ color: {C_TEXT}; font-weight: 500; }} /* ---- the three-channel key: the conceptual spine of the whole tool ---- */ .pts-channels {{ display: flex; gap: 10px; flex-wrap: wrap; }} .pts-chan {{ flex: 1 1 190px; border: 1px solid {C_LINE}; border-left: 2px solid var(--c); border-radius: 3px; background: {C_CANVAS}99; padding: 10px 13px; }} .pts-chan .k {{ font-family: 'IBM Plex Mono', monospace; font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--c); margin-bottom: 3px; }} .pts-chan .v {{ font-size: 12.5px; color: {C_TEXT}; line-height: 1.4; }} .pts-chan .m {{ font-size: 11px; color: {C_TEXT_FAINT}; margin-top: 4px; font-family: 'IBM Plex Mono', monospace; }} /* ---- stat tiles: tabular figures, instrument readout ---- */ .pts-stats {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 9px; margin: 4px 0 16px; }} .pts-stat {{ border: 1px solid {C_LINE}; border-radius: 3px; background: {C_PANEL}; padding: 11px 13px 10px; border-top: 2px solid var(--c, {C_LINE}); }} .pts-stat .l {{ font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; letter-spacing: 0.13em; text-transform: uppercase; color: {C_TEXT_FAINT}; margin-bottom: 5px; white-space: nowrap; }} .pts-stat .n {{ font-family: 'IBM Plex Mono', monospace; font-size: 22px; font-weight: 600; color: {C_TEXT}; font-variant-numeric: tabular-nums; line-height: 1.1; }} .pts-stat .n.accent {{ color: var(--c); }} .pts-stat .s {{ font-size: 10.5px; color: {C_TEXT_FAINT}; margin-top: 2px; }} /* ---- section rule ---- */ .pts-rule {{ font-family: 'IBM Plex Mono', monospace; font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; color: {C_TEXT_FAINT}; display: flex; align-items: center; gap: 12px; margin: 20px 0 10px; }} .pts-rule::after {{ content: ""; flex: 1; height: 1px; background: {C_LINE}; }} /* ---- event cards ---- */ .pts-card {{ border: 1px solid {C_LINE}; border-left: 3px solid var(--c, {C_NEUTRAL}); border-radius: 3px; background: {C_PANEL}; padding: 14px 16px; margin-bottom: 10px; }} .pts-card .hdr {{ display: flex; align-items: center; gap: 8px; margin-bottom: 9px; flex-wrap: wrap; }} .pts-badge {{ font-family: 'IBM Plex Mono', monospace; font-size: 9.5px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; padding: 3px 7px; border-radius: 2px; border: 1px solid var(--c, {C_NEUTRAL})55; color: var(--c, {C_NEUTRAL}); background: var(--c, {C_NEUTRAL})12; }} .pts-metric {{ font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-variant-numeric: tabular-nums; color: {C_TEXT_MUTED}; }} .pts-metric b {{ color: {C_TEXT}; font-weight: 600; }} /* the token, shown inline in its context */ .pts-context {{ font-family: 'IBM Plex Mono', monospace; font-size: 12.5px; line-height: 1.75; color: {C_TEXT_MUTED}; background: {C_CANVAS}; border: 1px solid {C_LINE_SOFT}; border-radius: 3px; padding: 12px 14px; white-space: pre-wrap; max-height: 260px; overflow-y: auto; }} .pts-hit {{ padding: 1px 4px; border-radius: 2px; font-weight: 600; color: #0A0D13; background: var(--c); box-shadow: 0 0 14px -2px var(--c); }} /* a latent readout is NOT a probability delta -- it never gets a valence colour */ .pts-obs {{ font-family: 'IBM Plex Mono', monospace; font-size: 10px; color: {C_LATENT}; border: 1px dashed {C_LATENT}44; border-radius: 2px; padding: 2px 6px; letter-spacing: 0.06em; }} /* ---- gradio surface tuning ---- */ .tabs button {{ font-family: 'IBM Plex Mono', monospace !important; font-size: 11.5px !important; letter-spacing: 0.09em !important; text-transform: uppercase !important; }} .tabs button.selected {{ color: {C_LATENT} !important; border-bottom: 2px solid {C_LATENT} !important; }} .block, .form {{ border-radius: 4px !important; }} label span {{ font-family: 'IBM Plex Mono', monospace !important; font-size: 10px !important; letter-spacing: 0.11em !important; text-transform: uppercase !important; color: {C_TEXT_FAINT} !important; }} .plot-container, .js-plotly-plot {{ border-radius: 3px; }} ::-webkit-scrollbar {{ width: 9px; height: 9px; }} ::-webkit-scrollbar-track {{ background: {C_CANVAS}; }} ::-webkit-scrollbar-thumb {{ background: {C_LINE}; border-radius: 5px; }} ::-webkit-scrollbar-thumb:hover {{ background: #2E3A50; }} /* ---- source bar: one compact row, so the charts stay above the fold ---- */ .pts-loadbar {{ border: 1px solid {C_LINE}; border-radius: 4px; background: {C_PANEL}; padding: 10px 12px; margin-bottom: 8px; align-items: center !important; }} .pts-loadbar .wrap, .pts-loadbar .file-preview {{ min-height: 0 !important; }} /* the drop zone is a secondary affordance -- shrink its chrome so it does not shout louder than the dataset picker next to it */ .pts-upload .wrap {{ font-size: 11px !important; gap: 2px !important; }} .pts-upload svg {{ width: 16px !important; height: 16px !important; }} .pts-upload .or {{ display: none !important; }} .pts-status textarea {{ background: {C_CANVAS} !important; border: 1px solid {C_LINE_SOFT} !important; font-family: 'IBM Plex Mono', monospace !important; font-size: 11px !important; color: {C_TEXT_MUTED} !important; padding: 7px 10px !important; }} /* ---- sliders: instrument, not stock ---- */ input[type=range] {{ accent-color: {C_LATENT}; }} .gradio-container input[type=range]::-webkit-slider-runnable-track {{ background: {C_LINE} !important; height: 3px !important; }} .gradio-container input[type=range]::-webkit-slider-thumb {{ background: {C_LATENT} !important; border: none !important; width: 13px !important; height: 13px !important; border-radius: 50% !important; margin-top: -5px !important; box-shadow: 0 0 10px -1px {C_LATENT}; }} /* numbers anywhere gradio renders them */ .gradio-container input[type=number] {{ font-family: 'IBM Plex Mono', monospace !important; font-variant-numeric: tabular-nums; }} """ MASTHEAD = f"""
PTSPIVOTAL TOKEN SEARCH

A causal-event search framework for model reasoning. PTS finds the pivotal reasoning events that shift a model's probability of solving a task, at three representational scales — latent, token, and sentence — as a single kind of object.

Latent PTS
Meta-tokens read out of the hidden workspace
J-lens · observational
Token PTS
Emitted tokens that flip success probability
ΔP(success) · measured
Sentence PTS
Reasoning steps that flip success probability
ΔP(success) · measured
""" with gr.Blocks(title="PTS · Pivotal Token Search", theme=PTS_THEME, css=CSS) as demo: gr.HTML(MASTHEAD) # Source bar. Kept to a single row: the old stacked layout ate the whole # viewport and pushed every chart below the fold, which is a worse problem # than any colour choice. with gr.Row(elem_classes="pts-loadbar"): with gr.Column(scale=2, min_width=190): source_type = gr.Radio( choices=["HuggingFace Hub", "Local File"], value="HuggingFace Hub", label="Source", container=False, ) with gr.Column(scale=5, min_width=280): dataset_dropdown = gr.Dropdown( choices=HF_DATASETS, value=DEFAULT_DATASET, label="Dataset", allow_custom_value=True, container=False, ) with gr.Column(scale=3, min_width=180): file_upload = gr.File( label="Or upload JSONL", file_types=[".jsonl", ".json"], height=88, elem_classes="pts-upload", ) with gr.Column(scale=2, min_width=140): load_btn = gr.Button("Load", variant="primary", size="sm") refresh_btn = gr.Button("Refresh", variant="secondary", size="sm") with gr.Row(): load_status = gr.Textbox(interactive=False, container=False, show_label=False, lines=1, max_lines=1, elem_classes="pts-status", placeholder="no dataset loaded") dataset_info = gr.Textbox(interactive=False, container=False, show_label=False, lines=1, max_lines=1, elem_classes="pts-status", placeholder="—") # Main Visualization Tabs with gr.Tabs(): # Overview Tab (unified event overview) with gr.TabItem("Overview"): gr.Markdown("### Dataset Statistics") gr.Markdown( "*For PTS datasets this counts events at all three scales " "(latent / token / sentence) and the causal links between them. " "Emitted Δ-probabilities and latent readout scores are charted " "separately: they are different quantities and are not comparable.*" ) stats_html = gr.HTML() stats_chart = gr.Plot() # Event Explorer Tab (generalization of the old Token Explorer) with gr.TabItem("Event Explorer"): gr.Markdown("### Explore Causal Reasoning Events") gr.Markdown( "*Filter events across all three scales. Latent meta-tokens are " "observational: they carry a **readout score**, not a probability delta, " "and are shown in purple rather than green/red.*" ) with gr.Row(): ev_type = gr.Dropdown( choices=["All"], value="All", label="Event Type" ) ev_granularity = gr.Dropdown( choices=["All", "latent", "token", "sentence"], value="All", label="Granularity" ) ev_polarity = gr.Radio( choices=["All", "Positive", "Negative", "Unscored (latent)"], value="All", label="Impact" ) with gr.Row(): ev_category = gr.Dropdown( choices=["All"], value="All", label="Category" ) ev_min_score = gr.Slider( minimum=0.0, maximum=1.0, step=0.01, value=0.0, label="Min Score (|Δ prob| for emitted, readout score for latent)" ) with gr.Row(): ev_layer_min = gr.Slider( minimum=0, maximum=128, step=1, value=0, label="Min Layer (latent events only)" ) ev_layer_max = gr.Slider( minimum=0, maximum=128, step=1, value=128, label="Max Layer (latent events only)" ) ev_summary = gr.HTML() ev_score_plot = gr.Plot(label="Score Distribution") gr.Markdown("#### Event Detail") with gr.Row(): with gr.Column(scale=1): token_slider = gr.Slider( minimum=0, maximum=100, step=1, value=0, label="Event Index (within current filter)" ) with gr.Column(scale=3): token_html = gr.HTML(label="Event in Context") prob_chart = gr.Plot(label="Probability Change / Readout Score") # Causal Event Graph Tab with gr.TabItem("Causal Event Graph") as tab_graph: gr.Markdown("### Causal Event Graph") gr.Markdown(""" *Latent meta-tokens (diamonds) → pivotal tokens (circles) → thought anchors (squares) → outcome (star). Edges come from the event links (`precedes_event_ids` / `linked_event_ids` / `parent_event_id`). Green = positive impact, red = negative impact, purple = latent (no measured valence). Node size reflects score. legacy datasets fall back to the original reasoning graph.* """) with gr.Row(): query_filter = gr.Dropdown( choices=[], value=None, label="Filter by Query" ) graph_plot = gr.Plot() # Embedding Visualization Tab with gr.TabItem("Embedding Space") as tab_embed: gr.Markdown("### Embedding Space Visualization") gr.Markdown("*t-SNE projection of sentence/token embeddings. Explore clusters and patterns.*") with gr.Row(): color_dropdown = gr.Dropdown( choices=["is_positive", "event_type", "granularity", "category", "sentence_category", "reasoning_pattern", "task_type"], value="is_positive", label="Color By" ) embed_plot = gr.Plot() # Reasoning Timeline Tab (was: Circuit Tracer) with gr.TabItem("Reasoning Timeline") as tab_timeline: gr.Markdown("### Reasoning Timeline") gr.Markdown( "*One shared generation axis across four scales: latent meta-tokens, " "emitted pivotal tokens, thought-anchor sentences, and the resulting " "success probability. Latent events are placed by their offset from the " "emitted event they precede.*" ) with gr.Row(): circuit_query_idx = gr.Slider( minimum=0, maximum=100, step=1, value=0, label="Query Index" ) timeline_plot = gr.Plot(label="Reasoning Timeline") gr.Markdown("#### Latent Workspace Heatmap") gr.Markdown( "*Meta-token (or category) x generation position, colored by **readout score** " "— how strongly the lens surfaces that concept. This is not a probability delta.*" ) heatmap_plot = gr.Plot(label="Workspace Heatmap") gr.Markdown("#### Step-by-Step Reasoning Circuit") circuit_html = gr.HTML() circuit_chart = gr.Plot() # Event handlers - using api_name=False to prevent schema generation issues load_btn.click( fn=load_dataset_action, inputs=[source_type, dataset_dropdown, file_upload], outputs=[load_status, dataset_info, stats_html, stats_chart, graph_plot, embed_plot, circuit_html, circuit_chart, timeline_plot, heatmap_plot, token_slider, query_filter, ev_type, ev_category, ev_summary, ev_score_plot, token_html, prob_chart], api_name=False ) # Lazy tab rendering: each heavy tab computes its figures only when opened, # so load never pays for all six at once. Recompute-on-open is cheap in # Python and keeps the wiring simple (no stale-cache bugs). def _current_df(): df = current_data.get("df") return df if df is not None and hasattr(df, "empty") and not df.empty else None def render_graph_tab(): df = _current_df() return create_causal_event_graph(df) if df is not None else _empty_fig("No data loaded.", 480) def render_embed_tab(): df = _current_df() return create_embedding_visualization(df) if df is not None else _empty_fig("No data loaded.", 480) def render_timeline_tab(): df = _current_df() if df is None: e = _empty_fig("No data loaded.", 480) return "", e, e, e first_q = df['query'].iloc[0] if 'query' in df.columns and len(df) else None c_html, c_fig = create_circuit_visualization(df) return c_html, c_fig, create_reasoning_timeline(df, first_q), create_workspace_heatmap(df, first_q) tab_graph.select(fn=render_graph_tab, outputs=[graph_plot], api_name=False) tab_embed.select(fn=render_embed_tab, outputs=[embed_plot], api_name=False) tab_timeline.select( fn=render_timeline_tab, outputs=[circuit_html, circuit_chart, timeline_plot, heatmap_plot], api_name=False, ) refresh_btn.click( fn=refresh_all, outputs=[stats_html, stats_chart, graph_plot, embed_plot, circuit_html, circuit_chart, timeline_plot, heatmap_plot], api_name=False ) token_slider.change( fn=get_event_details, inputs=[token_slider], outputs=[token_html, prob_chart], api_name=False ) event_filter_inputs = [ev_type, ev_granularity, ev_polarity, ev_category, ev_min_score, ev_layer_min, ev_layer_max] event_filter_outputs = [ev_summary, ev_score_plot, token_slider, token_html, prob_chart] for filter_component in event_filter_inputs: filter_component.change( fn=apply_event_filters, inputs=event_filter_inputs, outputs=event_filter_outputs, api_name=False ) query_filter.change( fn=update_graph_visualization, inputs=[query_filter], outputs=[graph_plot], api_name=False ) color_dropdown.change( fn=update_embedding_visualization, inputs=[color_dropdown], outputs=[embed_plot], api_name=False ) circuit_query_idx.change( fn=update_circuit_view, inputs=[circuit_query_idx], outputs=[circuit_html, circuit_chart, timeline_plot, heatmap_plot], api_name=False ) # ============================================================================ # Main Entry Point # ============================================================================ if __name__ == "__main__": demo.launch()