Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- app.py +318 -318
- src/extractor.py +223 -223
- src/graph_builder.py +222 -222
- src/sample_texts.py +76 -76
- src/visualizer.py +155 -155
app.py
CHANGED
|
@@ -11,10 +11,10 @@ import pandas as pd
|
|
| 11 |
from src.extractor import EntityExtractor
|
| 12 |
from src.graph_builder import KnowledgeGraph, ENTITY_COLORS
|
| 13 |
from src.visualizer import (
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
)
|
| 19 |
from src.sample_texts import SAMPLE_TEXTS
|
| 20 |
|
|
@@ -23,10 +23,10 @@ from src.sample_texts import SAMPLE_TEXTS
|
|
| 23 |
# ======================================================================
|
| 24 |
|
| 25 |
st.set_page_config(
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
)
|
| 31 |
|
| 32 |
# ======================================================================
|
|
@@ -34,111 +34,111 @@ st.set_page_config(
|
|
| 34 |
# ======================================================================
|
| 35 |
|
| 36 |
st.markdown(
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
)
|
| 143 |
|
| 144 |
|
|
@@ -147,68 +147,68 @@ st.markdown(
|
|
| 147 |
# ======================================================================
|
| 148 |
|
| 149 |
with st.sidebar:
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
|
| 213 |
|
| 214 |
# ======================================================================
|
|
@@ -216,25 +216,25 @@ with st.sidebar:
|
|
| 216 |
# ======================================================================
|
| 217 |
|
| 218 |
st.markdown(
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
)
|
| 223 |
st.markdown(
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
)
|
| 229 |
|
| 230 |
# Colour legend
|
| 231 |
legend_html = " ".join(
|
| 232 |
-
|
| 233 |
-
|
| 234 |
)
|
| 235 |
st.markdown(
|
| 236 |
-
|
| 237 |
-
|
| 238 |
)
|
| 239 |
|
| 240 |
|
|
@@ -244,23 +244,23 @@ st.markdown(
|
|
| 244 |
|
| 245 |
@st.cache_data(show_spinner=False)
|
| 246 |
def run_pipeline(text: str, types: tuple):
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
|
| 254 |
-
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
|
| 263 |
-
|
| 264 |
|
| 265 |
|
| 266 |
# ======================================================================
|
|
@@ -268,132 +268,132 @@ def run_pipeline(text: str, types: tuple):
|
|
| 268 |
# ======================================================================
|
| 269 |
|
| 270 |
if "has_run" not in st.session_state:
|
| 271 |
-
|
| 272 |
|
| 273 |
if build_clicked and text_to_process.strip():
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
|
| 278 |
# Auto-run on first visit with demo text
|
| 279 |
if not st.session_state.has_run and input_mode == "Demo Texts":
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
|
| 284 |
if st.session_state.has_run:
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
|
| 389 |
else:
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
|
|
|
| 11 |
from src.extractor import EntityExtractor
|
| 12 |
from src.graph_builder import KnowledgeGraph, ENTITY_COLORS
|
| 13 |
from src.visualizer import (
|
| 14 |
+
create_pyvis_graph,
|
| 15 |
+
graph_stats_chart,
|
| 16 |
+
centrality_chart,
|
| 17 |
+
community_chart,
|
| 18 |
)
|
| 19 |
from src.sample_texts import SAMPLE_TEXTS
|
| 20 |
|
|
|
|
| 23 |
# ======================================================================
|
| 24 |
|
| 25 |
st.set_page_config(
|
| 26 |
+
page_title="GraphMind | Knowledge Graph",
|
| 27 |
+
page_icon="G",
|
| 28 |
+
layout="wide",
|
| 29 |
+
initial_sidebar_state="expanded",
|
| 30 |
)
|
| 31 |
|
| 32 |
# ======================================================================
|
|
|
|
| 34 |
# ======================================================================
|
| 35 |
|
| 36 |
st.markdown(
|
| 37 |
+
"""
|
| 38 |
+
<style>
|
| 39 |
+
/* ---- Global ---- */
|
| 40 |
+
.stApp {
|
| 41 |
+
background-color: #0a0a0a;
|
| 42 |
+
color: #e0e0e0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/* ---- Sidebar ---- */
|
| 46 |
+
section[data-testid="stSidebar"] {
|
| 47 |
+
background-color: #111111;
|
| 48 |
+
border-right: 1px solid #1e1e1e;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* ---- Headers ---- */
|
| 52 |
+
h1, h2, h3, h4 {
|
| 53 |
+
color: #ffffff !important;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* ---- Metric cards ---- */
|
| 57 |
+
div[data-testid="stMetric"] {
|
| 58 |
+
background: linear-gradient(135deg, #111111 0%, #1a1a2e 100%);
|
| 59 |
+
border: 1px solid #1e1e1e;
|
| 60 |
+
border-radius: 12px;
|
| 61 |
+
padding: 16px 20px;
|
| 62 |
+
}
|
| 63 |
+
div[data-testid="stMetric"] label {
|
| 64 |
+
color: #888888 !important;
|
| 65 |
+
}
|
| 66 |
+
div[data-testid="stMetric"] div[data-testid="stMetricValue"] {
|
| 67 |
+
color: #00ff88 !important;
|
| 68 |
+
font-weight: 700;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* ---- Buttons ---- */
|
| 72 |
+
.stButton > button {
|
| 73 |
+
background: linear-gradient(135deg, #00ff88 0%, #00d4ff 100%);
|
| 74 |
+
color: #0a0a0a;
|
| 75 |
+
border: none;
|
| 76 |
+
border-radius: 8px;
|
| 77 |
+
font-weight: 700;
|
| 78 |
+
padding: 0.5rem 1.5rem;
|
| 79 |
+
transition: all 0.3s ease;
|
| 80 |
+
}
|
| 81 |
+
.stButton > button:hover {
|
| 82 |
+
transform: translateY(-2px);
|
| 83 |
+
box-shadow: 0 4px 20px rgba(0,255,136,0.3);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* ---- Tabs ---- */
|
| 87 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 88 |
+
gap: 8px;
|
| 89 |
+
}
|
| 90 |
+
.stTabs [data-baseweb="tab"] {
|
| 91 |
+
background-color: #1a1a1a;
|
| 92 |
+
border-radius: 8px 8px 0 0;
|
| 93 |
+
color: #888888;
|
| 94 |
+
padding: 8px 20px;
|
| 95 |
+
}
|
| 96 |
+
.stTabs [aria-selected="true"] {
|
| 97 |
+
background-color: #1e1e2e;
|
| 98 |
+
color: #00ff88 !important;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/* ---- DataFrame ---- */
|
| 102 |
+
.stDataFrame {
|
| 103 |
+
border: 1px solid #1e1e1e;
|
| 104 |
+
border-radius: 8px;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* ---- Expanders ---- */
|
| 108 |
+
.streamlit-expanderHeader {
|
| 109 |
+
background-color: #111111;
|
| 110 |
+
border-radius: 8px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/* ---- Success / info banners ---- */
|
| 114 |
+
.stAlert {
|
| 115 |
+
background-color: #111111;
|
| 116 |
+
border: 1px solid #1e1e1e;
|
| 117 |
+
border-radius: 8px;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ---- Accent text helpers ---- */
|
| 121 |
+
.accent-green { color: #00ff88; font-weight: 700; }
|
| 122 |
+
.accent-blue { color: #00d4ff; font-weight: 700; }
|
| 123 |
+
|
| 124 |
+
/* ---- Legend colour pills ---- */
|
| 125 |
+
.legend-pill {
|
| 126 |
+
display: inline-block;
|
| 127 |
+
padding: 3px 12px;
|
| 128 |
+
border-radius: 20px;
|
| 129 |
+
margin: 2px 4px;
|
| 130 |
+
font-size: 0.82rem;
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
color: #0a0a0a;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* ---- Divider ---- */
|
| 136 |
+
hr {
|
| 137 |
+
border-color: #1e1e1e;
|
| 138 |
+
}
|
| 139 |
+
</style>
|
| 140 |
+
""",
|
| 141 |
+
unsafe_allow_html=True,
|
| 142 |
)
|
| 143 |
|
| 144 |
|
|
|
|
| 147 |
# ======================================================================
|
| 148 |
|
| 149 |
with st.sidebar:
|
| 150 |
+
st.markdown("## GraphMind")
|
| 151 |
+
st.markdown(
|
| 152 |
+
"<span class='accent-green'>Knowledge Graph</span> "
|
| 153 |
+
"<span class='accent-blue'>Construction & Reasoning</span>",
|
| 154 |
+
unsafe_allow_html=True,
|
| 155 |
+
)
|
| 156 |
+
st.markdown("---")
|
| 157 |
+
|
| 158 |
+
# --- Input source ---
|
| 159 |
+
st.markdown("### Text Source")
|
| 160 |
+
input_mode = st.radio(
|
| 161 |
+
"Choose input method",
|
| 162 |
+
["Demo Texts", "Paste Your Own"],
|
| 163 |
+
label_visibility="collapsed",
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
text_to_process = ""
|
| 167 |
+
|
| 168 |
+
if input_mode == "Demo Texts":
|
| 169 |
+
selected_demo = st.selectbox(
|
| 170 |
+
"Select a demo text",
|
| 171 |
+
list(SAMPLE_TEXTS.keys()),
|
| 172 |
+
)
|
| 173 |
+
text_to_process = SAMPLE_TEXTS[selected_demo]
|
| 174 |
+
with st.expander("Preview text", expanded=False):
|
| 175 |
+
st.caption(text_to_process[:500] + "…")
|
| 176 |
+
else:
|
| 177 |
+
text_to_process = st.text_area(
|
| 178 |
+
"Paste your text below",
|
| 179 |
+
height=250,
|
| 180 |
+
placeholder="Enter text containing named entities…",
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
st.markdown("---")
|
| 184 |
+
|
| 185 |
+
# --- Extraction settings ---
|
| 186 |
+
st.markdown("### Extraction Settings")
|
| 187 |
+
entity_types = st.multiselect(
|
| 188 |
+
"Entity types to extract",
|
| 189 |
+
["PERSON", "ORG", "LOCATION", "DATE", "TECHNOLOGY"],
|
| 190 |
+
default=["PERSON", "ORG", "LOCATION", "DATE", "TECHNOLOGY"],
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
min_mentions = st.slider(
|
| 194 |
+
"Minimum mentions for nodes",
|
| 195 |
+
min_value=1,
|
| 196 |
+
max_value=5,
|
| 197 |
+
value=1,
|
| 198 |
+
help="Only show entities mentioned at least this many times.",
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
st.markdown("---")
|
| 202 |
+
|
| 203 |
+
# --- Build button ---
|
| 204 |
+
build_clicked = st.button(" Build Knowledge Graph", use_container_width=True)
|
| 205 |
+
|
| 206 |
+
st.markdown("---")
|
| 207 |
+
st.markdown(
|
| 208 |
+
"<div style='text-align:center;color:#555;font-size:0.75rem;'>"
|
| 209 |
+
"Built by <b>Yogesh Kuchimanchi</b><br>MIT License</div>",
|
| 210 |
+
unsafe_allow_html=True,
|
| 211 |
+
)
|
| 212 |
|
| 213 |
|
| 214 |
# ======================================================================
|
|
|
|
| 216 |
# ======================================================================
|
| 217 |
|
| 218 |
st.markdown(
|
| 219 |
+
"<h1 style='text-align:center;'>"
|
| 220 |
+
" Graph<span class='accent-green'>Mind</span></h1>",
|
| 221 |
+
unsafe_allow_html=True,
|
| 222 |
)
|
| 223 |
st.markdown(
|
| 224 |
+
"<p style='text-align:center;color:#888;margin-top:-10px;'>"
|
| 225 |
+
"Construct knowledge graphs from unstructured text using rule-based NER "
|
| 226 |
+
"and graph reasoning.</p>",
|
| 227 |
+
unsafe_allow_html=True,
|
| 228 |
)
|
| 229 |
|
| 230 |
# Colour legend
|
| 231 |
legend_html = " ".join(
|
| 232 |
+
f"<span class='legend-pill' style='background:{color};'>{label}</span>"
|
| 233 |
+
for label, color in ENTITY_COLORS.items()
|
| 234 |
)
|
| 235 |
st.markdown(
|
| 236 |
+
f"<div style='text-align:center;margin-bottom:20px;'>{legend_html}</div>",
|
| 237 |
+
unsafe_allow_html=True,
|
| 238 |
)
|
| 239 |
|
| 240 |
|
|
|
|
| 244 |
|
| 245 |
@st.cache_data(show_spinner=False)
|
| 246 |
def run_pipeline(text: str, types: tuple):
|
| 247 |
+
"""Run NER + graph construction and cache results."""
|
| 248 |
+
extractor = EntityExtractor()
|
| 249 |
+
entities = extractor.extract(text)
|
| 250 |
|
| 251 |
+
# Filter entity types
|
| 252 |
+
entities = [e for e in entities if e["label"] in types]
|
| 253 |
|
| 254 |
+
relationships = extractor.extract_relationships(text, entities)
|
| 255 |
|
| 256 |
+
kg = KnowledgeGraph()
|
| 257 |
+
kg.add_entities(entities)
|
| 258 |
+
kg.add_relationships(relationships)
|
| 259 |
|
| 260 |
+
stats = kg.get_stats()
|
| 261 |
+
graph_html = create_pyvis_graph(kg)
|
| 262 |
|
| 263 |
+
return entities, relationships, kg, stats, graph_html
|
| 264 |
|
| 265 |
|
| 266 |
# ======================================================================
|
|
|
|
| 268 |
# ======================================================================
|
| 269 |
|
| 270 |
if "has_run" not in st.session_state:
|
| 271 |
+
st.session_state.has_run = False
|
| 272 |
|
| 273 |
if build_clicked and text_to_process.strip():
|
| 274 |
+
st.session_state.has_run = True
|
| 275 |
+
st.session_state.text = text_to_process
|
| 276 |
+
st.session_state.types = tuple(entity_types)
|
| 277 |
|
| 278 |
# Auto-run on first visit with demo text
|
| 279 |
if not st.session_state.has_run and input_mode == "Demo Texts":
|
| 280 |
+
st.session_state.has_run = True
|
| 281 |
+
st.session_state.text = text_to_process
|
| 282 |
+
st.session_state.types = tuple(entity_types)
|
| 283 |
|
| 284 |
if st.session_state.has_run:
|
| 285 |
+
with st.spinner("Extracting entities and building graph…"):
|
| 286 |
+
entities, relationships, kg, stats, graph_html = run_pipeline(
|
| 287 |
+
st.session_state.text, st.session_state.types
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
# ==================================================================
|
| 291 |
+
# Metrics row
|
| 292 |
+
# ==================================================================
|
| 293 |
+
m1, m2, m3, m4 = st.columns(4)
|
| 294 |
+
m1.metric("Total Nodes", stats["total_nodes"])
|
| 295 |
+
m2.metric("Total Edges", stats["total_edges"])
|
| 296 |
+
m3.metric("Communities", stats["num_communities"])
|
| 297 |
+
m4.metric("Entity Types", len(stats["entity_type_counts"]))
|
| 298 |
+
|
| 299 |
+
st.markdown("---")
|
| 300 |
+
|
| 301 |
+
# ==================================================================
|
| 302 |
+
# Tabs
|
| 303 |
+
# ==================================================================
|
| 304 |
+
tab_graph, tab_entities, tab_relations, tab_stats = st.tabs(
|
| 305 |
+
[" Interactive Graph", " Entities", " Relationships", " Statistics"]
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
# --- Interactive Graph ---
|
| 309 |
+
with tab_graph:
|
| 310 |
+
st.markdown("#### Interactive Knowledge Graph")
|
| 311 |
+
st.caption("Drag, zoom, and hover nodes for details.")
|
| 312 |
+
components.html(graph_html, height=680, scrolling=False)
|
| 313 |
+
|
| 314 |
+
# --- Entities table ---
|
| 315 |
+
with tab_entities:
|
| 316 |
+
st.markdown("#### Extracted Entities")
|
| 317 |
+
if entities:
|
| 318 |
+
df_ent = pd.DataFrame(entities)
|
| 319 |
+
df_ent = df_ent[["text", "label", "start", "end"]]
|
| 320 |
+
df_ent.columns = ["Entity", "Type", "Start", "End"]
|
| 321 |
+
|
| 322 |
+
# Colour-coded type column
|
| 323 |
+
st.dataframe(
|
| 324 |
+
df_ent.style.apply(
|
| 325 |
+
lambda row: [
|
| 326 |
+
"",
|
| 327 |
+
f"color: {ENTITY_COLORS.get(row['Type'], '#888')}",
|
| 328 |
+
"",
|
| 329 |
+
"",
|
| 330 |
+
],
|
| 331 |
+
axis=1,
|
| 332 |
+
),
|
| 333 |
+
use_container_width=True,
|
| 334 |
+
height=450,
|
| 335 |
+
)
|
| 336 |
+
st.caption(f"Total: **{len(entities)}** entities extracted.")
|
| 337 |
+
else:
|
| 338 |
+
st.info("No entities found. Try different text or settings.")
|
| 339 |
+
|
| 340 |
+
# --- Relationships table ---
|
| 341 |
+
with tab_relations:
|
| 342 |
+
st.markdown("#### Extracted Relationships")
|
| 343 |
+
if relationships:
|
| 344 |
+
df_rel = pd.DataFrame(relationships)
|
| 345 |
+
df_rel = df_rel[["source", "relation", "target", "source_label", "target_label"]]
|
| 346 |
+
df_rel.columns = ["Source", "Relation", "Target", "Src Type", "Tgt Type"]
|
| 347 |
+
st.dataframe(df_rel, use_container_width=True, height=450)
|
| 348 |
+
st.caption(f"Total: **{len(relationships)}** relationships inferred.")
|
| 349 |
+
else:
|
| 350 |
+
st.info("No relationships found.")
|
| 351 |
+
|
| 352 |
+
# --- Statistics ---
|
| 353 |
+
with tab_stats:
|
| 354 |
+
st.markdown("#### Graph Analytics")
|
| 355 |
+
|
| 356 |
+
col_left, col_right = st.columns(2)
|
| 357 |
+
|
| 358 |
+
with col_left:
|
| 359 |
+
fig_dist = graph_stats_chart(stats)
|
| 360 |
+
st.plotly_chart(fig_dist, use_container_width=True)
|
| 361 |
+
|
| 362 |
+
with col_right:
|
| 363 |
+
fig_community = community_chart(stats["communities"])
|
| 364 |
+
st.plotly_chart(fig_community, use_container_width=True)
|
| 365 |
+
|
| 366 |
+
st.markdown("---")
|
| 367 |
+
fig_central = centrality_chart(stats["top_central_nodes"])
|
| 368 |
+
st.plotly_chart(fig_central, use_container_width=True)
|
| 369 |
+
|
| 370 |
+
with st.expander("Community Details"):
|
| 371 |
+
for i, comm in enumerate(stats["communities"]):
|
| 372 |
+
st.markdown(
|
| 373 |
+
f"**Community {i+1}** ({len(comm)} members): "
|
| 374 |
+
+ ", ".join(comm)
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
with st.expander("Raw Statistics"):
|
| 378 |
+
st.json(
|
| 379 |
+
{
|
| 380 |
+
"density": round(stats["density"], 6),
|
| 381 |
+
"total_nodes": stats["total_nodes"],
|
| 382 |
+
"total_edges": stats["total_edges"],
|
| 383 |
+
"entity_type_counts": stats["entity_type_counts"],
|
| 384 |
+
"relation_type_counts": stats["relation_type_counts"],
|
| 385 |
+
"num_communities": stats["num_communities"],
|
| 386 |
+
}
|
| 387 |
+
)
|
| 388 |
|
| 389 |
else:
|
| 390 |
+
# Placeholder when nothing has been processed yet
|
| 391 |
+
st.markdown(
|
| 392 |
+
"<div style='text-align:center;padding:80px 20px;color:#555;'>"
|
| 393 |
+
"<h3>Paste text or select a demo, then click "
|
| 394 |
+
"<span class='accent-green'>Build Knowledge Graph</span></h3>"
|
| 395 |
+
"<p>The engine will extract entities, infer relationships, "
|
| 396 |
+
"and visualise an interactive knowledge graph.</p>"
|
| 397 |
+
"</div>",
|
| 398 |
+
unsafe_allow_html=True,
|
| 399 |
+
)
|
src/extractor.py
CHANGED
|
@@ -15,250 +15,250 @@ from typing import List, Dict, Tuple
|
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
|
| 17 |
PERSON_PATTERNS = [
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
]
|
| 32 |
|
| 33 |
ORG_PATTERNS = [
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
]
|
| 45 |
|
| 46 |
LOCATION_PATTERNS = [
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
]
|
| 55 |
|
| 56 |
DATE_PATTERNS = [
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
]
|
| 69 |
|
| 70 |
TECHNOLOGY_PATTERNS = [
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
]
|
| 83 |
|
| 84 |
# Map label -> compiled patterns
|
| 85 |
ENTITY_PATTERNS: Dict[str, List[re.Pattern]] = {
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
}
|
| 92 |
|
| 93 |
# Words that should never be tagged as PERSON
|
| 94 |
PERSON_STOPWORDS = {
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
}
|
| 107 |
|
| 108 |
|
| 109 |
class EntityExtractor:
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
|
|
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
|
| 17 |
PERSON_PATTERNS = [
|
| 18 |
+
# Titles followed by capitalized names
|
| 19 |
+
r"(?:Dr|Prof|Mr|Mrs|Ms|Sir|Lord|President|CEO|CTO|Director)\.\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+",
|
| 20 |
+
# Common well-known names (seed list)
|
| 21 |
+
r"\b(?:Elon Musk|Jeff Bezos|Sam Altman|Demis Hassabis|Yann LeCun|Geoffrey Hinton|"
|
| 22 |
+
r"Fei-Fei Li|Andrew Ng|Ilya Sutskever|Jensen Huang|Satya Nadella|Tim Cook|"
|
| 23 |
+
r"Mark Zuckerberg|Sundar Pichai|Dario Amodei|Andrej Karpathy|"
|
| 24 |
+
r"Alan Turing|Ada Lovelace|John von Neumann|Claude Shannon|"
|
| 25 |
+
r"Albert Einstein|Isaac Newton|Marie Curie|Nikola Tesla|"
|
| 26 |
+
r"Napoleon Bonaparte|Winston Churchill|Abraham Lincoln|Mahatma Gandhi|"
|
| 27 |
+
r"Alexander Hamilton|Thomas Jefferson|Benjamin Franklin|George Washington|"
|
| 28 |
+
r"Leonardo da Vinci|Galileo Galilei|Charles Darwin|Stephen Hawking)\b",
|
| 29 |
+
# Two or three capitalized words that look like person names
|
| 30 |
+
r"\b[A-Z][a-z]{2,15}\s+(?:[A-Z]\.\s+)?[A-Z][a-z]{2,15}\b",
|
| 31 |
]
|
| 32 |
|
| 33 |
ORG_PATTERNS = [
|
| 34 |
+
r"\b(?:Google|Microsoft|Apple|Amazon|Meta|OpenAI|DeepMind|Anthropic|Tesla|"
|
| 35 |
+
r"NVIDIA|IBM|Intel|AMD|Qualcomm|Samsung|TSMC|Oracle|Salesforce|Adobe|"
|
| 36 |
+
r"Netflix|Spotify|Twitter|LinkedIn|GitHub|Stack Overflow|"
|
| 37 |
+
r"MIT|Stanford|Harvard|Oxford|Cambridge|Berkeley|Carnegie Mellon|"
|
| 38 |
+
r"NASA|CERN|WHO|UNESCO|United Nations|European Union|"
|
| 39 |
+
r"IEEE|ACM|NeurIPS|ICML|ICLR|AAAI|CVPR|"
|
| 40 |
+
r"Goldman Sachs|JPMorgan|Morgan Stanley|BlackRock)\b",
|
| 41 |
+
r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\s+(?:Inc|Corp|Ltd|LLC|Group|Foundation|"
|
| 42 |
+
r"Institute|University|Laboratory|Labs|Research|Association|Organization)\b",
|
| 43 |
+
r"\b(?:University|Institute|Academy)\s+of\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b",
|
| 44 |
]
|
| 45 |
|
| 46 |
LOCATION_PATTERNS = [
|
| 47 |
+
r"\b(?:New York|San Francisco|Silicon Valley|Los Angeles|Chicago|Boston|Seattle|"
|
| 48 |
+
r"Washington D\.C\.|London|Paris|Berlin|Tokyo|Beijing|Shanghai|Mumbai|"
|
| 49 |
+
r"Bangalore|Toronto|Montreal|Sydney|Singapore|Hong Kong|Dubai|"
|
| 50 |
+
r"California|Texas|Massachusetts|Virginia|"
|
| 51 |
+
r"United States|United Kingdom|China|India|Japan|Germany|France|Canada|"
|
| 52 |
+
r"Australia|South Korea|Israel|Switzerland|"
|
| 53 |
+
r"Europe|Asia|North America|South America|Africa)\b",
|
| 54 |
]
|
| 55 |
|
| 56 |
DATE_PATTERNS = [
|
| 57 |
+
# Full dates
|
| 58 |
+
r"\b(?:January|February|March|April|May|June|July|August|September|"
|
| 59 |
+
r"October|November|December)\s+\d{1,2},?\s+\d{4}\b",
|
| 60 |
+
# Month Year
|
| 61 |
+
r"\b(?:January|February|March|April|May|June|July|August|September|"
|
| 62 |
+
r"October|November|December)\s+\d{4}\b",
|
| 63 |
+
# Year ranges & standalone years
|
| 64 |
+
r"\b(?:19|20)\d{2}[-–]\d{2,4}\b",
|
| 65 |
+
r"\b(?:19|20)\d{2}s?\b",
|
| 66 |
+
# Relative dates
|
| 67 |
+
r"\b(?:Q[1-4]\s+\d{4})\b",
|
| 68 |
]
|
| 69 |
|
| 70 |
TECHNOLOGY_PATTERNS = [
|
| 71 |
+
r"\b(?:GPT-[0-9]+|GPT|BERT|Transformer|LLM|LLMs|DALL[-·]E|Stable Diffusion|"
|
| 72 |
+
r"ChatGPT|Copilot|AlphaFold|AlphaGo|"
|
| 73 |
+
r"Python|JavaScript|TypeScript|Rust|Go|Java|C\+\+|SQL|"
|
| 74 |
+
r"TensorFlow|PyTorch|Keras|scikit-learn|Hugging Face|LangChain|"
|
| 75 |
+
r"Kubernetes|Docker|AWS|Azure|GCP|"
|
| 76 |
+
r"blockchain|quantum computing|machine learning|deep learning|"
|
| 77 |
+
r"artificial intelligence|natural language processing|NLP|"
|
| 78 |
+
r"computer vision|reinforcement learning|neural network|neural networks|"
|
| 79 |
+
r"convolutional neural network|CNN|RNN|LSTM|GAN|GANs|"
|
| 80 |
+
r"large language model|retrieval-augmented generation|RAG|"
|
| 81 |
+
r"knowledge graph|attention mechanism|self-attention)\b",
|
| 82 |
]
|
| 83 |
|
| 84 |
# Map label -> compiled patterns
|
| 85 |
ENTITY_PATTERNS: Dict[str, List[re.Pattern]] = {
|
| 86 |
+
"TECHNOLOGY": [re.compile(p, re.IGNORECASE) for p in TECHNOLOGY_PATTERNS],
|
| 87 |
+
"ORG": [re.compile(p) for p in ORG_PATTERNS],
|
| 88 |
+
"LOCATION": [re.compile(p) for p in LOCATION_PATTERNS],
|
| 89 |
+
"DATE": [re.compile(p) for p in DATE_PATTERNS],
|
| 90 |
+
"PERSON": [re.compile(p) for p in PERSON_PATTERNS],
|
| 91 |
}
|
| 92 |
|
| 93 |
# Words that should never be tagged as PERSON
|
| 94 |
PERSON_STOPWORDS = {
|
| 95 |
+
"The", "This", "That", "These", "Those", "Here", "There",
|
| 96 |
+
"However", "Moreover", "Furthermore", "Although", "Because",
|
| 97 |
+
"While", "During", "After", "Before", "Since", "Within",
|
| 98 |
+
"Between", "Through", "About", "Their", "Where", "Which",
|
| 99 |
+
"Every", "Other", "Another", "First", "Second", "Third",
|
| 100 |
+
"Many", "Most", "Some", "Such", "Each", "Both", "Several",
|
| 101 |
+
"Recent", "Major", "Large", "Small", "High", "Early", "Late",
|
| 102 |
+
"With", "From", "Into", "Over", "Under", "Also", "Just",
|
| 103 |
+
"More", "Very", "Much", "Well", "Even", "Still", "Already",
|
| 104 |
+
"Knowledge Graph", "Construction", "Reasoning", "Engine",
|
| 105 |
+
"Research", "Development", "Analysis", "Processing", "Learning",
|
| 106 |
}
|
| 107 |
|
| 108 |
|
| 109 |
class EntityExtractor:
|
| 110 |
+
"""
|
| 111 |
+
Rule-based Named Entity Recognition engine.
|
| 112 |
+
|
| 113 |
+
Uses curated regex patterns to identify entities in text without
|
| 114 |
+
requiring large spaCy model downloads.
|
| 115 |
+
"""
|
| 116 |
+
|
| 117 |
+
def __init__(self):
|
| 118 |
+
self.patterns = ENTITY_PATTERNS
|
| 119 |
+
|
| 120 |
+
# ------------------------------------------------------------------
|
| 121 |
+
# Public API
|
| 122 |
+
# ------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
def extract(self, text: str) -> List[Dict]:
|
| 125 |
+
"""
|
| 126 |
+
Extract named entities from *text*.
|
| 127 |
+
|
| 128 |
+
Returns a list of dicts:
|
| 129 |
+
[{"text": ..., "label": ..., "start": ..., "end": ...}, ...]
|
| 130 |
+
"""
|
| 131 |
+
raw_entities: List[Dict] = []
|
| 132 |
+
|
| 133 |
+
for label, compiled_patterns in self.patterns.items():
|
| 134 |
+
for pattern in compiled_patterns:
|
| 135 |
+
for match in pattern.finditer(text):
|
| 136 |
+
entity_text = match.group().strip()
|
| 137 |
+
|
| 138 |
+
# Filter noisy PERSON matches
|
| 139 |
+
if label == "PERSON" and entity_text in PERSON_STOPWORDS:
|
| 140 |
+
continue
|
| 141 |
+
if label == "PERSON" and len(entity_text.split()) < 2:
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
+
raw_entities.append({
|
| 145 |
+
"text": entity_text,
|
| 146 |
+
"label": label,
|
| 147 |
+
"start": match.start(),
|
| 148 |
+
"end": match.end(),
|
| 149 |
+
})
|
| 150 |
+
|
| 151 |
+
# Deduplicate overlapping spans (prefer longer matches)
|
| 152 |
+
entities = self._resolve_overlaps(raw_entities)
|
| 153 |
+
return entities
|
| 154 |
+
|
| 155 |
+
def extract_relationships(
|
| 156 |
+
self, text: str, entities: List[Dict] | None = None
|
| 157 |
+
) -> List[Dict]:
|
| 158 |
+
"""
|
| 159 |
+
Infer relationships between entities via sentence co-occurrence.
|
| 160 |
+
|
| 161 |
+
Returns a list of dicts:
|
| 162 |
+
[{"source": ..., "target": ..., "relation": ..., "sentence": ...}, ...]
|
| 163 |
+
"""
|
| 164 |
+
if entities is None:
|
| 165 |
+
entities = self.extract(text)
|
| 166 |
+
|
| 167 |
+
sentences = self._split_sentences(text)
|
| 168 |
+
relationships: List[Dict] = []
|
| 169 |
+
seen: set = set()
|
| 170 |
+
|
| 171 |
+
for sentence in sentences:
|
| 172 |
+
# Find entities present in this sentence
|
| 173 |
+
present = [
|
| 174 |
+
e for e in entities
|
| 175 |
+
if e["text"] in sentence
|
| 176 |
+
]
|
| 177 |
+
|
| 178 |
+
for i, src in enumerate(present):
|
| 179 |
+
for tgt in present[i + 1:]:
|
| 180 |
+
key = (src["text"], tgt["text"])
|
| 181 |
+
if key in seen:
|
| 182 |
+
continue
|
| 183 |
+
seen.add(key)
|
| 184 |
+
|
| 185 |
+
relation = self._infer_relation(src, tgt, sentence)
|
| 186 |
+
relationships.append({
|
| 187 |
+
"source": src["text"],
|
| 188 |
+
"target": tgt["text"],
|
| 189 |
+
"source_label": src["label"],
|
| 190 |
+
"target_label": tgt["label"],
|
| 191 |
+
"relation": relation,
|
| 192 |
+
"sentence": sentence.strip(),
|
| 193 |
+
})
|
| 194 |
+
|
| 195 |
+
return relationships
|
| 196 |
+
|
| 197 |
+
# ------------------------------------------------------------------
|
| 198 |
+
# Internal helpers
|
| 199 |
+
# ------------------------------------------------------------------
|
| 200 |
+
|
| 201 |
+
@staticmethod
|
| 202 |
+
def _resolve_overlaps(entities: List[Dict]) -> List[Dict]:
|
| 203 |
+
"""Keep the longest span when two entities overlap."""
|
| 204 |
+
# Sort by start, then by descending length
|
| 205 |
+
entities.sort(key=lambda e: (e["start"], -(e["end"] - e["start"])))
|
| 206 |
+
result: List[Dict] = []
|
| 207 |
+
last_end = -1
|
| 208 |
+
for ent in entities:
|
| 209 |
+
if ent["start"] >= last_end:
|
| 210 |
+
result.append(ent)
|
| 211 |
+
last_end = ent["end"]
|
| 212 |
+
return result
|
| 213 |
+
|
| 214 |
+
@staticmethod
|
| 215 |
+
def _split_sentences(text: str) -> List[str]:
|
| 216 |
+
"""Naive sentence splitter."""
|
| 217 |
+
return re.split(r"(?<=[.!?])\s+", text)
|
| 218 |
+
|
| 219 |
+
@staticmethod
|
| 220 |
+
def _infer_relation(src: Dict, tgt: Dict, sentence: str) -> str:
|
| 221 |
+
"""Heuristic relation labelling based on entity types and context."""
|
| 222 |
+
pair = (src["label"], tgt["label"])
|
| 223 |
+
|
| 224 |
+
# Keyword-based relation detection
|
| 225 |
+
s_lower = sentence.lower()
|
| 226 |
+
|
| 227 |
+
if any(kw in s_lower for kw in ["founded", "co-founded", "started", "created"]):
|
| 228 |
+
if pair in [("PERSON", "ORG"), ("PERSON", "TECHNOLOGY")]:
|
| 229 |
+
return "FOUNDED"
|
| 230 |
+
if any(kw in s_lower for kw in ["acquired", "bought", "purchased", "merged"]):
|
| 231 |
+
return "ACQUIRED"
|
| 232 |
+
if any(kw in s_lower for kw in ["works at", "joined", "hired", "employed"]):
|
| 233 |
+
return "WORKS_AT"
|
| 234 |
+
if any(kw in s_lower for kw in ["located in", "based in", "headquartered"]):
|
| 235 |
+
return "LOCATED_IN"
|
| 236 |
+
if any(kw in s_lower for kw in ["developed", "built", "designed", "invented"]):
|
| 237 |
+
return "DEVELOPED"
|
| 238 |
+
if any(kw in s_lower for kw in ["published", "released", "announced", "launched"]):
|
| 239 |
+
return "RELEASED"
|
| 240 |
+
if any(kw in s_lower for kw in ["uses", "using", "powered by", "built on", "leverages"]):
|
| 241 |
+
return "USES"
|
| 242 |
+
if any(kw in s_lower for kw in ["competed", "versus", "rivaling", "competing"]):
|
| 243 |
+
return "COMPETES_WITH"
|
| 244 |
+
if any(kw in s_lower for kw in ["collaborated", "partnered", "partnership"]):
|
| 245 |
+
return "COLLABORATES_WITH"
|
| 246 |
+
if any(kw in s_lower for kw in ["invested", "funding", "backed"]):
|
| 247 |
+
return "INVESTED_IN"
|
| 248 |
+
|
| 249 |
+
# Fallback: type-pair heuristics
|
| 250 |
+
relation_map = {
|
| 251 |
+
("PERSON", "ORG"): "AFFILIATED_WITH",
|
| 252 |
+
("PERSON", "TECHNOLOGY"): "WORKS_ON",
|
| 253 |
+
("PERSON", "LOCATION"): "LOCATED_IN",
|
| 254 |
+
("ORG", "TECHNOLOGY"): "DEVELOPS",
|
| 255 |
+
("ORG", "LOCATION"): "LOCATED_IN",
|
| 256 |
+
("ORG", "ORG"): "RELATED_TO",
|
| 257 |
+
("TECHNOLOGY", "TECHNOLOGY"): "RELATED_TO",
|
| 258 |
+
("PERSON", "PERSON"): "ASSOCIATED_WITH",
|
| 259 |
+
("PERSON", "DATE"): "ACTIVE_IN",
|
| 260 |
+
("ORG", "DATE"): "ACTIVE_IN",
|
| 261 |
+
("TECHNOLOGY", "DATE"): "EMERGED_IN",
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
return relation_map.get(pair, relation_map.get((tgt["label"], src["label"]), "RELATED_TO"))
|
src/graph_builder.py
CHANGED
|
@@ -13,232 +13,232 @@ import json
|
|
| 13 |
|
| 14 |
# Colour palette for entity types
|
| 15 |
ENTITY_COLORS = {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
}
|
| 22 |
|
| 23 |
ENTITY_SHAPES = {
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
}
|
| 30 |
|
| 31 |
|
| 32 |
class KnowledgeGraph:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
|
|
|
| 13 |
|
| 14 |
# Colour palette for entity types
|
| 15 |
ENTITY_COLORS = {
|
| 16 |
+
"PERSON": "#00ff88",
|
| 17 |
+
"ORG": "#00d4ff",
|
| 18 |
+
"LOCATION": "#a855f7",
|
| 19 |
+
"TECHNOLOGY": "#f59e0b",
|
| 20 |
+
"DATE": "#ec4899",
|
| 21 |
}
|
| 22 |
|
| 23 |
ENTITY_SHAPES = {
|
| 24 |
+
"PERSON": "dot",
|
| 25 |
+
"ORG": "diamond",
|
| 26 |
+
"LOCATION": "triangle",
|
| 27 |
+
"TECHNOLOGY": "square",
|
| 28 |
+
"DATE": "star",
|
| 29 |
}
|
| 30 |
|
| 31 |
|
| 32 |
class KnowledgeGraph:
|
| 33 |
+
"""
|
| 34 |
+
A directed knowledge graph backed by ``networkx.DiGraph``.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self):
|
| 38 |
+
self.graph = nx.DiGraph()
|
| 39 |
+
|
| 40 |
+
# ------------------------------------------------------------------
|
| 41 |
+
# Construction
|
| 42 |
+
# ------------------------------------------------------------------
|
| 43 |
+
|
| 44 |
+
def add_entities(self, entities: List[Dict]) -> None:
|
| 45 |
+
"""
|
| 46 |
+
Add entity nodes to the graph.
|
| 47 |
+
|
| 48 |
+
Parameters
|
| 49 |
+
----------
|
| 50 |
+
entities : list of dict
|
| 51 |
+
Each dict must contain at least ``text`` and ``label`` keys.
|
| 52 |
+
"""
|
| 53 |
+
for ent in entities:
|
| 54 |
+
node_id = ent["text"]
|
| 55 |
+
if self.graph.has_node(node_id):
|
| 56 |
+
# Increment mention count
|
| 57 |
+
self.graph.nodes[node_id]["mentions"] = (
|
| 58 |
+
self.graph.nodes[node_id].get("mentions", 1) + 1
|
| 59 |
+
)
|
| 60 |
+
continue
|
| 61 |
+
|
| 62 |
+
self.graph.add_node(
|
| 63 |
+
node_id,
|
| 64 |
+
label=ent["label"],
|
| 65 |
+
color=ENTITY_COLORS.get(ent["label"], "#888888"),
|
| 66 |
+
shape=ENTITY_SHAPES.get(ent["label"], "dot"),
|
| 67 |
+
mentions=1,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def add_relationships(self, relationships: List[Dict]) -> None:
|
| 71 |
+
"""
|
| 72 |
+
Add directed edges (relationships) to the graph.
|
| 73 |
+
|
| 74 |
+
Parameters
|
| 75 |
+
----------
|
| 76 |
+
relationships : list of dict
|
| 77 |
+
Each dict needs ``source``, ``target``, ``relation`` keys.
|
| 78 |
+
"""
|
| 79 |
+
for rel in relationships:
|
| 80 |
+
src, tgt = rel["source"], rel["target"]
|
| 81 |
+
|
| 82 |
+
# Ensure nodes exist
|
| 83 |
+
if not self.graph.has_node(src):
|
| 84 |
+
self.graph.add_node(
|
| 85 |
+
src,
|
| 86 |
+
label=rel.get("source_label", "UNKNOWN"),
|
| 87 |
+
color=ENTITY_COLORS.get(rel.get("source_label"), "#888888"),
|
| 88 |
+
shape=ENTITY_SHAPES.get(rel.get("source_label"), "dot"),
|
| 89 |
+
mentions=1,
|
| 90 |
+
)
|
| 91 |
+
if not self.graph.has_node(tgt):
|
| 92 |
+
self.graph.add_node(
|
| 93 |
+
tgt,
|
| 94 |
+
label=rel.get("target_label", "UNKNOWN"),
|
| 95 |
+
color=ENTITY_COLORS.get(rel.get("target_label"), "#888888"),
|
| 96 |
+
shape=ENTITY_SHAPES.get(rel.get("target_label"), "dot"),
|
| 97 |
+
mentions=1,
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
if self.graph.has_edge(src, tgt):
|
| 101 |
+
self.graph.edges[src, tgt]["weight"] = (
|
| 102 |
+
self.graph.edges[src, tgt].get("weight", 1) + 1
|
| 103 |
+
)
|
| 104 |
+
else:
|
| 105 |
+
self.graph.add_edge(
|
| 106 |
+
src,
|
| 107 |
+
tgt,
|
| 108 |
+
relation=rel["relation"],
|
| 109 |
+
weight=1,
|
| 110 |
+
sentence=rel.get("sentence", ""),
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
# ------------------------------------------------------------------
|
| 114 |
+
# Queries & Analytics
|
| 115 |
+
# ------------------------------------------------------------------
|
| 116 |
+
|
| 117 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 118 |
+
"""Return summary statistics of the knowledge graph."""
|
| 119 |
+
G = self.graph
|
| 120 |
+
|
| 121 |
+
label_counts: Dict[str, int] = {}
|
| 122 |
+
for _, data in G.nodes(data=True):
|
| 123 |
+
lbl = data.get("label", "UNKNOWN")
|
| 124 |
+
label_counts[lbl] = label_counts.get(lbl, 0) + 1
|
| 125 |
+
|
| 126 |
+
relation_counts: Dict[str, int] = {}
|
| 127 |
+
for _, _, data in G.edges(data=True):
|
| 128 |
+
rel = data.get("relation", "UNKNOWN")
|
| 129 |
+
relation_counts[rel] = relation_counts.get(rel, 0) + 1
|
| 130 |
+
|
| 131 |
+
communities = self.get_communities()
|
| 132 |
+
|
| 133 |
+
# Degree centrality for top nodes
|
| 134 |
+
if G.number_of_nodes() > 0:
|
| 135 |
+
centrality = nx.degree_centrality(G)
|
| 136 |
+
top_nodes = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:10]
|
| 137 |
+
else:
|
| 138 |
+
top_nodes = []
|
| 139 |
+
|
| 140 |
+
return {
|
| 141 |
+
"total_nodes": G.number_of_nodes(),
|
| 142 |
+
"total_edges": G.number_of_edges(),
|
| 143 |
+
"entity_type_counts": label_counts,
|
| 144 |
+
"relation_type_counts": relation_counts,
|
| 145 |
+
"num_communities": len(communities),
|
| 146 |
+
"communities": communities,
|
| 147 |
+
"top_central_nodes": top_nodes,
|
| 148 |
+
"density": nx.density(G) if G.number_of_nodes() > 1 else 0,
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
def get_communities(self) -> List[List[str]]:
|
| 152 |
+
"""
|
| 153 |
+
Detect communities using the greedy modularity algorithm
|
| 154 |
+
on the undirected projection.
|
| 155 |
+
"""
|
| 156 |
+
if self.graph.number_of_nodes() == 0:
|
| 157 |
+
return []
|
| 158 |
+
|
| 159 |
+
undirected = self.graph.to_undirected()
|
| 160 |
+
try:
|
| 161 |
+
from networkx.algorithms.community import greedy_modularity_communities
|
| 162 |
+
communities = greedy_modularity_communities(undirected)
|
| 163 |
+
return [sorted(list(c)) for c in communities]
|
| 164 |
+
except Exception:
|
| 165 |
+
# Fallback: connected components
|
| 166 |
+
return [sorted(list(c)) for c in nx.connected_components(undirected)]
|
| 167 |
+
|
| 168 |
+
def get_node_details(self, node_id: str) -> Optional[Dict]:
|
| 169 |
+
"""Return all attributes for a single node."""
|
| 170 |
+
if not self.graph.has_node(node_id):
|
| 171 |
+
return None
|
| 172 |
+
data = dict(self.graph.nodes[node_id])
|
| 173 |
+
data["id"] = node_id
|
| 174 |
+
data["in_degree"] = self.graph.in_degree(node_id)
|
| 175 |
+
data["out_degree"] = self.graph.out_degree(node_id)
|
| 176 |
+
data["neighbors"] = list(self.graph.successors(node_id)) + list(
|
| 177 |
+
self.graph.predecessors(node_id)
|
| 178 |
+
)
|
| 179 |
+
return data
|
| 180 |
+
|
| 181 |
+
# ------------------------------------------------------------------
|
| 182 |
+
# Export
|
| 183 |
+
# ------------------------------------------------------------------
|
| 184 |
+
|
| 185 |
+
def to_pyvis(self, height: str = "600px", width: str = "100%") -> Network:
|
| 186 |
+
"""
|
| 187 |
+
Convert the graph to a PyVis ``Network`` for interactive
|
| 188 |
+
HTML visualisation.
|
| 189 |
+
"""
|
| 190 |
+
net = Network(
|
| 191 |
+
height=height,
|
| 192 |
+
width=width,
|
| 193 |
+
directed=True,
|
| 194 |
+
bgcolor="#0a0a0a",
|
| 195 |
+
font_color="white",
|
| 196 |
+
select_menu=False,
|
| 197 |
+
filter_menu=False,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
net.barnes_hut(
|
| 201 |
+
gravity=-8000,
|
| 202 |
+
central_gravity=0.3,
|
| 203 |
+
spring_length=200,
|
| 204 |
+
spring_strength=0.05,
|
| 205 |
+
damping=0.09,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
for node_id, data in self.graph.nodes(data=True):
|
| 209 |
+
mentions = data.get("mentions", 1)
|
| 210 |
+
size = 15 + mentions * 5
|
| 211 |
+
net.add_node(
|
| 212 |
+
node_id,
|
| 213 |
+
label=node_id,
|
| 214 |
+
color=data.get("color", "#888888"),
|
| 215 |
+
shape=data.get("shape", "dot"),
|
| 216 |
+
size=min(size, 50),
|
| 217 |
+
title=f"{data.get('label', 'UNKNOWN')}\nMentions: {mentions}",
|
| 218 |
+
font={"size": 14, "color": "white"},
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
for src, tgt, data in self.graph.edges(data=True):
|
| 222 |
+
relation = data.get("relation", "")
|
| 223 |
+
weight = data.get("weight", 1)
|
| 224 |
+
net.add_edge(
|
| 225 |
+
src,
|
| 226 |
+
tgt,
|
| 227 |
+
title=relation,
|
| 228 |
+
label=relation,
|
| 229 |
+
width=min(weight * 1.5, 6),
|
| 230 |
+
color={"color": "#444444", "highlight": "#00ff88"},
|
| 231 |
+
font={"size": 10, "color": "#888888", "align": "middle"},
|
| 232 |
+
arrows={"to": {"enabled": True, "scaleFactor": 0.5}},
|
| 233 |
+
smooth={"type": "curvedCW", "roundness": 0.2},
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
return net
|
| 237 |
+
|
| 238 |
+
def to_dict(self) -> Dict:
|
| 239 |
+
"""Serialise the graph to a JSON-safe dictionary."""
|
| 240 |
+
return nx.node_link_data(self.graph)
|
| 241 |
+
|
| 242 |
+
def from_dict(self, data: Dict) -> None:
|
| 243 |
+
"""Load a graph from a dictionary produced by ``to_dict``."""
|
| 244 |
+
self.graph = nx.node_link_graph(data)
|
src/sample_texts.py
CHANGED
|
@@ -7,91 +7,91 @@ relationships, designed to showcase the knowledge graph pipeline.
|
|
| 7 |
|
| 8 |
SAMPLE_TEXTS = {
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
}
|
|
|
|
| 7 |
|
| 8 |
SAMPLE_TEXTS = {
|
| 9 |
|
| 10 |
+
# ------------------------------------------------------------------
|
| 11 |
+
"AI Research Paper Abstract": (
|
| 12 |
+
"In recent years, the field of artificial intelligence has undergone a "
|
| 13 |
+
"remarkable transformation driven by large language models. Geoffrey Hinton "
|
| 14 |
+
"and Yann LeCun, both pioneers in deep learning, laid the groundwork that "
|
| 15 |
+
"enabled breakthroughs such as the Transformer architecture. Their research "
|
| 16 |
+
"at the University of Toronto and Meta respectively has shaped the entire "
|
| 17 |
+
"landscape of modern neural networks.\n\n"
|
| 18 |
|
| 19 |
+
"OpenAI, founded in San Francisco in 2015, developed GPT-4 — a large language "
|
| 20 |
+
"model that demonstrated unprecedented capabilities in natural language processing. "
|
| 21 |
+
"Sam Altman, as CEO of OpenAI, oversaw the release of ChatGPT in November 2022, "
|
| 22 |
+
"which reached 100 million users within two months. Meanwhile, Google DeepMind, "
|
| 23 |
+
"led by Demis Hassabis in London, released AlphaFold, revolutionising protein "
|
| 24 |
+
"structure prediction using deep learning.\n\n"
|
| 25 |
|
| 26 |
+
"At Stanford University in California, Fei-Fei Li and Andrew Ng pioneered "
|
| 27 |
+
"computer vision and online machine learning education. Their contributions to "
|
| 28 |
+
"TensorFlow and PyTorch ecosystems accelerated the adoption of neural networks "
|
| 29 |
+
"across both academia and industry. The NeurIPS 2024 conference showcased over "
|
| 30 |
+
"3,000 papers on topics ranging from reinforcement learning to retrieval-augmented "
|
| 31 |
+
"generation.\n\n"
|
| 32 |
|
| 33 |
+
"Anthropic, founded by Dario Amodei in San Francisco, has focused on AI safety "
|
| 34 |
+
"research. Their work on constitutional AI and RLHF (Reinforcement Learning from "
|
| 35 |
+
"Human Feedback) represents a paradigm shift in how large language models are "
|
| 36 |
+
"aligned with human values. The collaboration between Anthropic and Google has "
|
| 37 |
+
"further strengthened the responsible AI movement in Silicon Valley."
|
| 38 |
+
),
|
| 39 |
|
| 40 |
+
# ------------------------------------------------------------------
|
| 41 |
+
"Tech Industry News": (
|
| 42 |
+
"NVIDIA, headquartered in Santa Clara, California, reported record revenue "
|
| 43 |
+
"driven by surging demand for its AI accelerator chips. Jensen Huang, CEO of "
|
| 44 |
+
"NVIDIA, announced new partnerships with Microsoft and Amazon to power their "
|
| 45 |
+
"cloud-based machine learning infrastructure on AWS and Azure. The company's "
|
| 46 |
+
"GPU technology has become the backbone of deep learning research worldwide.\n\n"
|
| 47 |
|
| 48 |
+
"In a landmark deal, Microsoft, led by CEO Satya Nadella, invested $10 billion "
|
| 49 |
+
"in OpenAI in January 2023, securing exclusive rights to integrate GPT-4 into "
|
| 50 |
+
"its products. GitHub Copilot, powered by OpenAI's Codex model, has transformed "
|
| 51 |
+
"software development, with over 1 million developers using the AI-powered "
|
| 52 |
+
"coding assistant built on the Transformer architecture.\n\n"
|
| 53 |
|
| 54 |
+
"Apple, under Tim Cook, has taken a different approach to artificial intelligence, "
|
| 55 |
+
"focusing on on-device machine learning and privacy-first AI. At their headquarters "
|
| 56 |
+
"in Cupertino, California, Apple's research team has developed advanced natural "
|
| 57 |
+
"language processing models that run entirely on iPhone hardware without cloud "
|
| 58 |
+
"connectivity. Samsung and Qualcomm have pursued similar edge AI strategies.\n\n"
|
| 59 |
|
| 60 |
+
"The European Union introduced the AI Act in 2024, establishing the world's first "
|
| 61 |
+
"comprehensive regulatory framework for artificial intelligence. The legislation "
|
| 62 |
+
"affects major tech companies operating in Europe, including Google, Meta, and "
|
| 63 |
+
"Amazon. Mark Zuckerberg responded by open-sourcing Meta's LLaMA models, while "
|
| 64 |
+
"Sundar Pichai pledged Google's commitment to responsible AI development in "
|
| 65 |
+
"partnership with UNESCO and the United Nations."
|
| 66 |
+
),
|
| 67 |
|
| 68 |
+
# ------------------------------------------------------------------
|
| 69 |
+
"Historical Event": (
|
| 70 |
+
"The development of computing in the 20th century stands as one of humanity's "
|
| 71 |
+
"greatest intellectual achievements. Alan Turing, working at Cambridge University "
|
| 72 |
+
"in the 1930s, formulated the concept of a universal computing machine that could "
|
| 73 |
+
"simulate any algorithmic process. His work at Bletchley Park during World War II "
|
| 74 |
+
"on breaking the Enigma cipher demonstrated the practical power of computation.\n\n"
|
| 75 |
|
| 76 |
+
"John von Neumann, a mathematician at the Institute for Advanced Study in "
|
| 77 |
+
"Princeton, designed the architecture that bears his name in 1945. The von Neumann "
|
| 78 |
+
"architecture — separating memory, processing, and input/output — remains the "
|
| 79 |
+
"foundation of virtually every modern computer. Claude Shannon, working at Bell "
|
| 80 |
+
"Labs in New Jersey, simultaneously developed information theory, establishing "
|
| 81 |
+
"the mathematical framework for digital communication.\n\n"
|
| 82 |
|
| 83 |
+
"Ada Lovelace, often regarded as the first computer programmer, wrote algorithms "
|
| 84 |
+
"for Charles Babbage's Analytical Engine in London during the 1840s. Her vision "
|
| 85 |
+
"of machines that could manipulate symbols beyond mere calculation anticipated "
|
| 86 |
+
"modern artificial intelligence by over a century. The United Kingdom has since "
|
| 87 |
+
"honored her legacy with Ada Lovelace Day, celebrated annually in October.\n\n"
|
| 88 |
|
| 89 |
+
"The founding of the Internet can be traced to ARPANET, developed by the United "
|
| 90 |
+
"States Department of Defense in the 1960s. Tim Berners-Lee, working at CERN in "
|
| 91 |
+
"Switzerland, invented the World Wide Web in 1989. His creation transformed how "
|
| 92 |
+
"humanity shares knowledge. Today, the convergence of quantum computing, "
|
| 93 |
+
"artificial intelligence, and blockchain technology promises yet another "
|
| 94 |
+
"revolution, with IBM, Google, and Microsoft leading the charge in quantum "
|
| 95 |
+
"computing research from laboratories across North America and Europe."
|
| 96 |
+
),
|
| 97 |
}
|
src/visualizer.py
CHANGED
|
@@ -18,28 +18,28 @@ from src.graph_builder import KnowledgeGraph, ENTITY_COLORS
|
|
| 18 |
# ------------------------------------------------------------------
|
| 19 |
|
| 20 |
def create_pyvis_graph(kg: KnowledgeGraph, height: str = "650px") -> str:
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
|
| 44 |
|
| 45 |
# ------------------------------------------------------------------
|
|
@@ -47,146 +47,146 @@ def create_pyvis_graph(kg: KnowledgeGraph, height: str = "650px") -> str:
|
|
| 47 |
# ------------------------------------------------------------------
|
| 48 |
|
| 49 |
_CHART_LAYOUT = dict(
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
)
|
| 56 |
|
| 57 |
|
| 58 |
def graph_stats_chart(stats: Dict[str, Any]) -> go.Figure:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
|
| 121 |
|
| 122 |
def centrality_chart(top_nodes: list) -> go.Figure:
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
|
| 161 |
|
| 162 |
def community_chart(communities: list) -> go.Figure:
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
| 18 |
# ------------------------------------------------------------------
|
| 19 |
|
| 20 |
def create_pyvis_graph(kg: KnowledgeGraph, height: str = "650px") -> str:
|
| 21 |
+
"""
|
| 22 |
+
Render *kg* as an interactive PyVis graph and return raw HTML.
|
| 23 |
+
|
| 24 |
+
The HTML string can be embedded directly with
|
| 25 |
+
``streamlit.components.v1.html()``.
|
| 26 |
+
"""
|
| 27 |
+
net = kg.to_pyvis(height=height)
|
| 28 |
+
|
| 29 |
+
# Generate HTML string (PyVis >= 0.3 supports generate_html)
|
| 30 |
+
try:
|
| 31 |
+
html = net.generate_html()
|
| 32 |
+
except AttributeError:
|
| 33 |
+
# Fallback for older pyvis
|
| 34 |
+
import tempfile, os
|
| 35 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8")
|
| 36 |
+
net.save_graph(tmp.name)
|
| 37 |
+
tmp.close()
|
| 38 |
+
with open(tmp.name, "r", encoding="utf-8") as f:
|
| 39 |
+
html = f.read()
|
| 40 |
+
os.unlink(tmp.name)
|
| 41 |
+
|
| 42 |
+
return html
|
| 43 |
|
| 44 |
|
| 45 |
# ------------------------------------------------------------------
|
|
|
|
| 47 |
# ------------------------------------------------------------------
|
| 48 |
|
| 49 |
_CHART_LAYOUT = dict(
|
| 50 |
+
paper_bgcolor="#0a0a0a",
|
| 51 |
+
plot_bgcolor="#111111",
|
| 52 |
+
font_color="white",
|
| 53 |
+
font_size=13,
|
| 54 |
+
margin=dict(l=40, r=40, t=50, b=40),
|
| 55 |
)
|
| 56 |
|
| 57 |
|
| 58 |
def graph_stats_chart(stats: Dict[str, Any]) -> go.Figure:
|
| 59 |
+
"""
|
| 60 |
+
Create a combined Plotly figure with:
|
| 61 |
+
- Entity type distribution (bar)
|
| 62 |
+
- Relationship type distribution (bar)
|
| 63 |
+
"""
|
| 64 |
+
entity_counts = stats.get("entity_type_counts", {})
|
| 65 |
+
relation_counts = stats.get("relation_type_counts", {})
|
| 66 |
+
|
| 67 |
+
fig = make_subplots(
|
| 68 |
+
rows=1,
|
| 69 |
+
cols=2,
|
| 70 |
+
subplot_titles=("Entity Types", "Relationship Types"),
|
| 71 |
+
horizontal_spacing=0.15,
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# --- Entity type bar chart ---
|
| 75 |
+
if entity_counts:
|
| 76 |
+
types = list(entity_counts.keys())
|
| 77 |
+
counts = list(entity_counts.values())
|
| 78 |
+
colors = [ENTITY_COLORS.get(t, "#888888") for t in types]
|
| 79 |
+
|
| 80 |
+
fig.add_trace(
|
| 81 |
+
go.Bar(
|
| 82 |
+
x=types,
|
| 83 |
+
y=counts,
|
| 84 |
+
marker_color=colors,
|
| 85 |
+
text=counts,
|
| 86 |
+
textposition="outside",
|
| 87 |
+
name="Entity Types",
|
| 88 |
+
showlegend=False,
|
| 89 |
+
),
|
| 90 |
+
row=1,
|
| 91 |
+
col=1,
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# --- Relationship type bar chart ---
|
| 95 |
+
if relation_counts:
|
| 96 |
+
rels = list(relation_counts.keys())
|
| 97 |
+
rcounts = list(relation_counts.values())
|
| 98 |
+
|
| 99 |
+
fig.add_trace(
|
| 100 |
+
go.Bar(
|
| 101 |
+
x=rels,
|
| 102 |
+
y=rcounts,
|
| 103 |
+
marker_color="#00d4ff",
|
| 104 |
+
text=rcounts,
|
| 105 |
+
textposition="outside",
|
| 106 |
+
name="Relationships",
|
| 107 |
+
showlegend=False,
|
| 108 |
+
),
|
| 109 |
+
row=1,
|
| 110 |
+
col=2,
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
fig.update_layout(
|
| 114 |
+
height=370,
|
| 115 |
+
**_CHART_LAYOUT,
|
| 116 |
+
)
|
| 117 |
+
fig.update_xaxes(tickangle=-40)
|
| 118 |
+
|
| 119 |
+
return fig
|
| 120 |
|
| 121 |
|
| 122 |
def centrality_chart(top_nodes: list) -> go.Figure:
|
| 123 |
+
"""
|
| 124 |
+
Horizontal bar chart of the top-N most central nodes.
|
| 125 |
+
"""
|
| 126 |
+
if not top_nodes:
|
| 127 |
+
fig = go.Figure()
|
| 128 |
+
fig.update_layout(
|
| 129 |
+
title="No nodes to display",
|
| 130 |
+
**_CHART_LAYOUT,
|
| 131 |
+
height=300,
|
| 132 |
+
)
|
| 133 |
+
return fig
|
| 134 |
+
|
| 135 |
+
names = [n[0] for n in reversed(top_nodes)]
|
| 136 |
+
values = [round(n[1], 4) for n in reversed(top_nodes)]
|
| 137 |
+
|
| 138 |
+
fig = go.Figure(
|
| 139 |
+
go.Bar(
|
| 140 |
+
x=values,
|
| 141 |
+
y=names,
|
| 142 |
+
orientation="h",
|
| 143 |
+
marker=dict(
|
| 144 |
+
color=values,
|
| 145 |
+
colorscale=[[0, "#0a0a0a"], [0.5, "#00d4ff"], [1, "#00ff88"]],
|
| 146 |
+
),
|
| 147 |
+
text=[f"{v:.3f}" for v in values],
|
| 148 |
+
textposition="outside",
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
fig.update_layout(
|
| 153 |
+
title="Top Nodes by Degree Centrality",
|
| 154 |
+
xaxis_title="Centrality Score",
|
| 155 |
+
height=max(300, len(top_nodes) * 35 + 100),
|
| 156 |
+
**_CHART_LAYOUT,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
return fig
|
| 160 |
|
| 161 |
|
| 162 |
def community_chart(communities: list) -> go.Figure:
|
| 163 |
+
"""Pie chart showing community sizes."""
|
| 164 |
+
if not communities:
|
| 165 |
+
fig = go.Figure()
|
| 166 |
+
fig.update_layout(title="No communities detected", **_CHART_LAYOUT, height=300)
|
| 167 |
+
return fig
|
| 168 |
+
|
| 169 |
+
labels = [f"Community {i+1}" for i in range(len(communities))]
|
| 170 |
+
sizes = [len(c) for c in communities]
|
| 171 |
+
|
| 172 |
+
fig = go.Figure(
|
| 173 |
+
go.Pie(
|
| 174 |
+
labels=labels,
|
| 175 |
+
values=sizes,
|
| 176 |
+
hole=0.45,
|
| 177 |
+
marker=dict(
|
| 178 |
+
colors=["#00ff88", "#00d4ff", "#a855f7", "#f59e0b", "#ec4899",
|
| 179 |
+
"#6366f1", "#14b8a6", "#f43f5e", "#84cc16", "#06b6d4"],
|
| 180 |
+
),
|
| 181 |
+
textinfo="label+percent",
|
| 182 |
+
textfont_size=12,
|
| 183 |
+
)
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
fig.update_layout(
|
| 187 |
+
title="Community Distribution",
|
| 188 |
+
height=370,
|
| 189 |
+
**_CHART_LAYOUT,
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
return fig
|