"""
GraphMind — Knowledge Graph Construction & Reasoning Engine
============================================================
Main Streamlit application.
"""
import streamlit as st
import streamlit.components.v1 as components
import pandas as pd
from src.extractor import EntityExtractor
from src.graph_builder import KnowledgeGraph, ENTITY_COLORS
from src.visualizer import (
create_pyvis_graph,
graph_stats_chart,
centrality_chart,
community_chart,
)
from src.sample_texts import SAMPLE_TEXTS
# ======================================================================
# Page configuration
# ======================================================================
st.set_page_config(
page_title="GraphMind | Knowledge Graph",
page_icon="G",
layout="wide",
initial_sidebar_state="expanded",
)
# ======================================================================
# Custom CSS — dark theme with accent colours
# ======================================================================
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# ======================================================================
# Sidebar
# ======================================================================
with st.sidebar:
st.markdown("## GraphMind")
st.markdown(
"Knowledge Graph "
"Construction & Reasoning",
unsafe_allow_html=True,
)
st.markdown("---")
# --- Input source ---
st.markdown("### Text Source")
input_mode = st.radio(
"Choose input method",
["Demo Texts", "Paste Your Own"],
label_visibility="collapsed",
)
text_to_process = ""
if input_mode == "Demo Texts":
selected_demo = st.selectbox(
"Select a demo text",
list(SAMPLE_TEXTS.keys()),
)
text_to_process = SAMPLE_TEXTS[selected_demo]
with st.expander("Preview text", expanded=False):
st.caption(text_to_process[:500] + "…")
else:
text_to_process = st.text_area(
"Paste your text below",
height=250,
placeholder="Enter text containing named entities…",
)
st.markdown("---")
# --- Extraction settings ---
st.markdown("### Extraction Settings")
entity_types = st.multiselect(
"Entity types to extract",
["PERSON", "ORG", "LOCATION", "DATE", "TECHNOLOGY"],
default=["PERSON", "ORG", "LOCATION", "DATE", "TECHNOLOGY"],
)
min_mentions = st.slider(
"Minimum mentions for nodes",
min_value=1,
max_value=5,
value=1,
help="Only show entities mentioned at least this many times.",
)
st.markdown("---")
# --- Build button ---
build_clicked = st.button(" Build Knowledge Graph", use_container_width=True)
st.markdown("---")
st.markdown(
"
"
"Built by Yogesh Kuchimanchi
MIT License
",
unsafe_allow_html=True,
)
# ======================================================================
# Main area — Header
# ======================================================================
st.markdown(
""
" GraphMind
",
unsafe_allow_html=True,
)
st.markdown(
""
"Construct knowledge graphs from unstructured text using rule-based NER "
"and graph reasoning.
",
unsafe_allow_html=True,
)
# Colour legend
legend_html = " ".join(
f"{label}"
for label, color in ENTITY_COLORS.items()
)
st.markdown(
f"{legend_html}
",
unsafe_allow_html=True,
)
# ======================================================================
# Processing pipeline
# ======================================================================
@st.cache_data(show_spinner=False)
def run_pipeline(text: str, types: tuple):
"""Run NER + graph construction and cache results."""
extractor = EntityExtractor()
entities = extractor.extract(text)
# Filter entity types
entities = [e for e in entities if e["label"] in types]
relationships = extractor.extract_relationships(text, entities)
kg = KnowledgeGraph()
kg.add_entities(entities)
kg.add_relationships(relationships)
stats = kg.get_stats()
graph_html = create_pyvis_graph(kg)
return entities, relationships, kg, stats, graph_html
# ======================================================================
# Run on button click OR first load with demo text
# ======================================================================
if "has_run" not in st.session_state:
st.session_state.has_run = False
if build_clicked and text_to_process.strip():
st.session_state.has_run = True
st.session_state.text = text_to_process
st.session_state.types = tuple(entity_types)
# Auto-run on first visit with demo text
if not st.session_state.has_run and input_mode == "Demo Texts":
st.session_state.has_run = True
st.session_state.text = text_to_process
st.session_state.types = tuple(entity_types)
if st.session_state.has_run:
with st.spinner("Extracting entities and building graph…"):
entities, relationships, kg, stats, graph_html = run_pipeline(
st.session_state.text, st.session_state.types
)
# ==================================================================
# Metrics row
# ==================================================================
m1, m2, m3, m4 = st.columns(4)
m1.metric("Total Nodes", stats["total_nodes"])
m2.metric("Total Edges", stats["total_edges"])
m3.metric("Communities", stats["num_communities"])
m4.metric("Entity Types", len(stats["entity_type_counts"]))
st.markdown("---")
# ==================================================================
# Tabs
# ==================================================================
tab_graph, tab_entities, tab_relations, tab_stats = st.tabs(
[" Interactive Graph", " Entities", " Relationships", " Statistics"]
)
# --- Interactive Graph ---
with tab_graph:
st.markdown("#### Interactive Knowledge Graph")
st.caption("Drag, zoom, and hover nodes for details.")
components.html(graph_html, height=680, scrolling=False)
# --- Entities table ---
with tab_entities:
st.markdown("#### Extracted Entities")
if entities:
df_ent = pd.DataFrame(entities)
df_ent = df_ent[["text", "label", "start", "end"]]
df_ent.columns = ["Entity", "Type", "Start", "End"]
# Colour-coded type column
st.dataframe(
df_ent.style.apply(
lambda row: [
"",
f"color: {ENTITY_COLORS.get(row['Type'], '#888')}",
"",
"",
],
axis=1,
),
use_container_width=True,
height=450,
)
st.caption(f"Total: **{len(entities)}** entities extracted.")
else:
st.info("No entities found. Try different text or settings.")
# --- Relationships table ---
with tab_relations:
st.markdown("#### Extracted Relationships")
if relationships:
df_rel = pd.DataFrame(relationships)
df_rel = df_rel[["source", "relation", "target", "source_label", "target_label"]]
df_rel.columns = ["Source", "Relation", "Target", "Src Type", "Tgt Type"]
st.dataframe(df_rel, use_container_width=True, height=450)
st.caption(f"Total: **{len(relationships)}** relationships inferred.")
else:
st.info("No relationships found.")
# --- Statistics ---
with tab_stats:
st.markdown("#### Graph Analytics")
col_left, col_right = st.columns(2)
with col_left:
fig_dist = graph_stats_chart(stats)
st.plotly_chart(fig_dist, use_container_width=True)
with col_right:
fig_community = community_chart(stats["communities"])
st.plotly_chart(fig_community, use_container_width=True)
st.markdown("---")
fig_central = centrality_chart(stats["top_central_nodes"])
st.plotly_chart(fig_central, use_container_width=True)
with st.expander("Community Details"):
for i, comm in enumerate(stats["communities"]):
st.markdown(
f"**Community {i+1}** ({len(comm)} members): "
+ ", ".join(comm)
)
with st.expander("Raw Statistics"):
st.json(
{
"density": round(stats["density"], 6),
"total_nodes": stats["total_nodes"],
"total_edges": stats["total_edges"],
"entity_type_counts": stats["entity_type_counts"],
"relation_type_counts": stats["relation_type_counts"],
"num_communities": stats["num_communities"],
}
)
else:
# Placeholder when nothing has been processed yet
st.markdown(
""
"
Paste text or select a demo, then click "
"Build Knowledge Graph
"
"
The engine will extract entities, infer relationships, "
"and visualise an interactive knowledge graph.
"
"
",
unsafe_allow_html=True,
)