Yogesh18018 commited on
Commit
09141b1
·
verified ·
1 Parent(s): 8e0e1d3

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. app.py +318 -318
  2. src/extractor.py +223 -223
  3. src/graph_builder.py +222 -222
  4. src/sample_texts.py +76 -76
  5. 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
- create_pyvis_graph,
15
- graph_stats_chart,
16
- centrality_chart,
17
- community_chart,
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
- page_title="GraphMind | Knowledge Graph",
27
- page_icon="G",
28
- layout="wide",
29
- initial_sidebar_state="expanded",
30
  )
31
 
32
  # ======================================================================
@@ -34,111 +34,111 @@ st.set_page_config(
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,68 +147,68 @@ st.markdown(
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,25 +216,25 @@ with st.sidebar:
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,23 +244,23 @@ st.markdown(
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,132 +268,132 @@ def run_pipeline(text: str, types: tuple):
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
- )
 
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
- # 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"))
 
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
- "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)
 
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
- "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
  }
 
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
- 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,146 +47,146 @@ def create_pyvis_graph(kg: KnowledgeGraph, height: str = "650px") -> str:
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
 
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