Jomaric commited on
Commit
c31efdf
·
0 Parent(s):

feat: initial app release

Browse files
Files changed (3) hide show
  1. README.md +23 -0
  2. app.py +440 -0
  3. requirements.txt +6 -0
README.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Concept Knowledge Graph Builder
3
+ emoji: 🕸️
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: gradio
7
+ python_version: "3.10"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Concept Knowledge Graph Builder
14
+
15
+ This is a premium, lightweight, interactive Concept and Entity network mapping tool designed specifically for computational social science and humanities courses.
16
+
17
+ ## Features
18
+ - **Flexible Data Input**: Paste raw text directly or upload datasets (`.csv`, `.xlsx`, or `.txt`).
19
+ - **Flexible Network Extraction Modes**:
20
+ - **Local Noun-Chunk Parser (CPU & Fast)**: Runs high-speed extraction utilizing SpaCy dependency mapping and noun chunks to extract key ideas/entities and calculate sentence-level co-occurrences locally on CPU.
21
+ - **Transformers (AI Mode)**: Leverages advanced generative models (like `Qwen2.5`) via Hugging Face's Serverless API with your personal access token to generate structured relational triple JSON structures.
22
+ - **Beautiful Graph Visualizations**: Renders complete, fully responsive interactive 2D circular network graphs using Plotly Scatter Network layers natively.
23
+ - **CSV Data Export**: Easily export extracted node tables and edge relationship files as standard CSV files.
app.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import numpy as np
5
+ import re
6
+ import json
7
+ import plotly.graph_objects as go
8
+ from huggingface_hub import InferenceClient
9
+
10
+ # Load or download spaCy English model dynamically
11
+ import spacy
12
+ try:
13
+ nlp = spacy.load("en_core_web_sm")
14
+ except OSError:
15
+ import spacy.cli
16
+ spacy.cli.download("en_core_web_sm")
17
+ nlp = spacy.load("en_core_web_sm")
18
+
19
+ def load_data(file_obj):
20
+ """Safely loads CSV, Excel, or TXT file into a Pandas DataFrame."""
21
+ if file_obj is None:
22
+ return None, gr.update(choices=[], visible=False), "Please upload a file."
23
+
24
+ file_path = file_obj.name
25
+ ext = os.path.splitext(file_path)[1].lower()
26
+
27
+ try:
28
+ if ext == '.csv':
29
+ df = pd.read_csv(file_path)
30
+ elif ext in ['.xls', '.xlsx']:
31
+ df = pd.read_excel(file_path)
32
+ elif ext == '.txt':
33
+ with open(file_path, 'r', encoding='utf-8') as f:
34
+ content = f.read()
35
+ df = pd.DataFrame({'text': [content]})
36
+ else:
37
+ return None, gr.update(choices=[], visible=False), "Unsupported file format. Please upload .csv, .xlsx, or .txt."
38
+
39
+ string_cols = [col for col in df.columns if df[col].dtype == 'object' or df[col].astype(str).str.len().mean() > 5]
40
+ if not string_cols:
41
+ string_cols = list(df.columns)
42
+
43
+ return df, gr.update(choices=string_cols, value=string_cols[0], visible=True), f"Successfully loaded dataset with {len(df)} rows."
44
+ except Exception as e:
45
+ return None, gr.update(choices=[], visible=False), f"Error loading file: {str(e)}"
46
+
47
+ def run_local_kg(text, min_edge_weight=1, max_nodes=25):
48
+ """Local SpaCy-based co-occurrence extractor that builds a Concept Knowledge Graph."""
49
+ doc = nlp(text)
50
+
51
+ # Extract entities and key noun chunks as concept nodes
52
+ concepts = []
53
+ for ent in doc.ents:
54
+ if ent.label_ in ["PERSON", "ORG", "GPE", "NORP", "FAC", "PRODUCT", "EVENT", "WORK_OF_ART"]:
55
+ concepts.append(ent.text.strip())
56
+
57
+ for chunk in doc.noun_chunks:
58
+ # Filter out pronouns and very short chunks
59
+ chunk_text = chunk.text.strip().lower()
60
+ if len(chunk_text.split()) <= 3 and chunk.root.pos_ != "PRON" and len(chunk_text) > 3:
61
+ concepts.append(chunk.text.strip())
62
+
63
+ # Standardize concept names (capitalize first letters)
64
+ concepts = [c.title() for c in concepts if len(c) > 2]
65
+
66
+ # We find which concepts co-occur within the same sentence
67
+ sentences = list(doc.sents)
68
+ edges = {}
69
+
70
+ for sent in sentences:
71
+ sent_text = sent.text.title()
72
+ # Find which unique concepts appear in this sentence
73
+ present_concepts = list(set([c for c in concepts if c in sent_text]))
74
+
75
+ # Build pairwise links
76
+ for i in range(len(present_concepts)):
77
+ for j in range(i+1, len(present_concepts)):
78
+ c1, c2 = present_concepts[i], present_concepts[j]
79
+ if c1 == c2:
80
+ continue
81
+ pair = tuple(sorted([c1, c2]))
82
+ edges[pair] = edges.get(pair, 0) + 1
83
+
84
+ # Filter edges by minimum weight
85
+ filtered_edges = {k: v for k, v in edges.items() if v >= min_edge_weight}
86
+
87
+ if not filtered_edges:
88
+ return pd.DataFrame(), pd.DataFrame(), None
89
+
90
+ # Get top nodes based on degree
91
+ node_degrees = {}
92
+ for (source, target), weight in filtered_edges.items():
93
+ node_degrees[source] = node_degrees.get(source, 0) + weight
94
+ node_degrees[target] = node_degrees.get(target, 0) + weight
95
+
96
+ top_nodes = sorted(node_degrees.items(), key=lambda x: x[1], reverse=True)[:max_nodes]
97
+ top_nodes_list = [n[0] for n in top_nodes]
98
+
99
+ # Keep only edges containing top nodes
100
+ final_edges = []
101
+ for (source, target), weight in filtered_edges.items():
102
+ if source in top_nodes_list and target in top_nodes_list:
103
+ final_edges.append({
104
+ "Source": source,
105
+ "Target": target,
106
+ "Relationship": "Co-occurrence",
107
+ "Weight": weight
108
+ })
109
+
110
+ df_edges = pd.DataFrame(final_edges)
111
+ df_nodes = pd.DataFrame([{"Node": n, "Importance (Degree)": d} for n, d in top_nodes])
112
+
113
+ # Build Plotly Network Layout (Circular Layout)
114
+ fig = go.Figure()
115
+
116
+ # 1. Position nodes in a circle
117
+ node_positions = {}
118
+ n_nodes = len(top_nodes_list)
119
+ for idx, node in enumerate(top_nodes_list):
120
+ angle = 2 * np.pi * idx / n_nodes
121
+ x = np.cos(angle)
122
+ y = np.sin(angle)
123
+ node_positions[node] = (x, y)
124
+
125
+ # 2. Draw edge lines
126
+ edge_x = []
127
+ edge_y = []
128
+ for edge in final_edges:
129
+ x0, y0 = node_positions[edge["Source"]]
130
+ x1, y1 = node_positions[edge["Target"]]
131
+ edge_x.extend([x0, x1, None])
132
+ edge_y.extend([y0, y1, None])
133
+
134
+ fig.add_trace(go.Scatter(
135
+ x=edge_x, y=edge_y,
136
+ line=dict(width=1.5, color='#334155'),
137
+ hoverinfo='none',
138
+ mode='lines'
139
+ ))
140
+
141
+ # 3. Draw nodes markers
142
+ node_x = []
143
+ node_y = []
144
+ node_text = []
145
+ node_sizes = []
146
+
147
+ for node, degree in top_nodes:
148
+ x, y = node_positions[node]
149
+ node_x.append(x)
150
+ node_y.append(y)
151
+ node_text.append(f"{node} (Degree: {degree})")
152
+ # scale marker size
153
+ node_sizes.append(15 + degree * 3)
154
+
155
+ fig.add_trace(go.Scatter(
156
+ x=node_x, y=node_y,
157
+ mode='markers+text',
158
+ hoverinfo='text',
159
+ text=top_nodes_list,
160
+ textposition="top center",
161
+ textfont=dict(color='#f3f4f6', size=10),
162
+ hovertext=node_text,
163
+ marker=dict(
164
+ showscale=True,
165
+ colorscale='Viridis',
166
+ color=node_sizes,
167
+ size=node_sizes,
168
+ colorbar=dict(
169
+ thickness=15,
170
+ title='Concept Connectivity',
171
+ xanchor='left',
172
+ titleside='right'
173
+ ),
174
+ line_width=2
175
+ )
176
+ ))
177
+
178
+ fig.update_layout(
179
+ title="Interactive Concept Knowledge Graph",
180
+ showlegend=False,
181
+ hovermode='closest',
182
+ margin=dict(b=20,l=5,r=5,t=40),
183
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
184
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
185
+ template="plotly_dark",
186
+ height=500
187
+ )
188
+
189
+ return df_nodes, df_edges, fig
190
+
191
+ def run_neural_kg(text, hf_token, model_name, max_nodes=20):
192
+ """Uses advanced generative instruction model to extract rich semantic relation triples."""
193
+ if not hf_token:
194
+ raise ValueError("Hugging Face API Access Token is required for Transformers mode.")
195
+
196
+ client = InferenceClient(token=hf_token)
197
+ prompt = f"""[INST] Extract main concept entities and their relationships from this text.
198
+ Return a clean, valid JSON list of objects with the keys "Source", "Relationship", and "Target" (limit to top {max_nodes} relationships).
199
+ Do not output extra text, markdown indicators, or commentary.
200
+
201
+ Text to parse:
202
+ "{text}" [/INST]"""
203
+
204
+ try:
205
+ response = client.text_generation(prompt, model=model_name, max_new_tokens=500, temperature=0.2)
206
+ # Parse JSON
207
+ json_clean = re.sub(r'```json\s*|\s*```', '', response).strip()
208
+ data = json.loads(json_clean)
209
+ df_edges = pd.DataFrame(data)
210
+
211
+ # Standardize columns
212
+ df_edges.columns = ["Source", "Relationship", "Target"]
213
+ df_edges["Weight"] = 1 # constant weight
214
+
215
+ # Extract unique nodes
216
+ nodes = list(set(df_edges["Source"].tolist() + df_edges["Target"].tolist()))
217
+ df_nodes = pd.DataFrame([{"Node": n, "Importance (Degree)": 1} for n in nodes])
218
+
219
+ # Circular layout Plotly graph
220
+ fig = go.Figure()
221
+ node_positions = {}
222
+ n_nodes = len(nodes)
223
+ for idx, node in enumerate(nodes):
224
+ angle = 2 * np.pi * idx / n_nodes
225
+ x = np.cos(angle)
226
+ y = np.sin(angle)
227
+ node_positions[node] = (x, y)
228
+
229
+ edge_x = []
230
+ edge_y = []
231
+ for idx, row in df_edges.iterrows():
232
+ x0, y0 = node_positions[row["Source"]]
233
+ x1, y1 = node_positions[row["Target"]]
234
+ edge_x.extend([x0, x1, None])
235
+ edge_y.extend([y0, y1, None])
236
+
237
+ fig.add_trace(go.Scatter(
238
+ x=edge_x, y=edge_y,
239
+ line=dict(width=1.5, color='#475569'),
240
+ hoverinfo='none',
241
+ mode='lines'
242
+ ))
243
+
244
+ node_x = []
245
+ node_y = []
246
+ for node in nodes:
247
+ x, y = node_positions[node]
248
+ node_x.append(x)
249
+ node_y.append(y)
250
+
251
+ fig.add_trace(go.Scatter(
252
+ x=node_x, y=node_y,
253
+ mode='markers+text',
254
+ hoverinfo='text',
255
+ text=nodes,
256
+ textposition="top center",
257
+ textfont=dict(color='#f3f4f6', size=10),
258
+ hovertext=nodes,
259
+ marker=dict(
260
+ color='#818cf8',
261
+ size=20,
262
+ line_width=2
263
+ )
264
+ ))
265
+
266
+ fig.update_layout(
267
+ title="Interactive Concept Knowledge Graph",
268
+ showlegend=False,
269
+ hovermode='closest',
270
+ margin=dict(b=20,l=5,r=5,t=40),
271
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
272
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
273
+ template="plotly_dark",
274
+ height=500
275
+ )
276
+
277
+ return df_nodes, df_edges, fig
278
+
279
+ except Exception as e:
280
+ raise RuntimeError(f"Hugging Face API or parsing error: {str(e)}")
281
+
282
+ def analyze_kg(text_input, file_obj, text_col, method, hf_token, hf_model, min_weight, max_nodes):
283
+ docs = []
284
+ if file_obj is not None:
285
+ df, _, _ = load_data(file_obj)
286
+ if df is not None and text_col in df.columns:
287
+ docs = df[text_col].astype(str).fillna("").tolist()
288
+ elif text_input and text_input.strip():
289
+ docs = [text_input]
290
+
291
+ if not docs:
292
+ return None, None, None, None, "Please enter text or upload a valid dataset first."
293
+
294
+ try:
295
+ if method == "Local Noun-Chunk Parser (CPU & Fast)":
296
+ df_nodes, df_edges, fig = run_local_kg(docs[0], min_weight, max_nodes)
297
+ else:
298
+ df_nodes, df_edges, fig = run_neural_kg(docs[0], hf_token, hf_model, max_nodes)
299
+
300
+ if df_nodes.empty:
301
+ return None, None, None, None, "No semantic concepts were successfully extracted. Try entering longer text or lowering the 'Min Co-occurrence' filter."
302
+
303
+ # Save edge CSV
304
+ csv_edges = "extracted_concept_edges.csv"
305
+ df_edges.to_csv(csv_edges, index=False)
306
+
307
+ status_md = f"Successfully generated Concept Knowledge Graph with **{len(df_nodes)}** nodes and **{len(df_edges)}** relationships."
308
+
309
+ return df_nodes, df_edges, fig, csv_edges, status_md
310
+
311
+ except Exception as e:
312
+ return None, None, None, None, f"Execution failed: {str(e)}"
313
+
314
+ custom_css = """
315
+ body {
316
+ background-color: #0b0f19;
317
+ color: #f3f4f6;
318
+ }
319
+ .gradio-container {
320
+ font-family: 'Inter', sans-serif !important;
321
+ }
322
+ h1, h2 {
323
+ color: #6366f1 !important;
324
+ }
325
+ """
326
+
327
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="indigo", secondary_hue="slate"), css=custom_css) as demo:
328
+ df_state = gr.State()
329
+
330
+ gr.HTML("""
331
+ <div style="text-align: center; margin-bottom: 2rem;">
332
+ <h1 style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; background: linear-gradient(to right, #6366f1, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Concept Knowledge Graph Builder</h1>
333
+ <p style="font-size: 1.1rem; color: #94a3b8; max-width: 800px; margin: 0 auto;">
334
+ Map out networks of people, locations, events, and abstract ideas in computational humanities.
335
+ Automatically extract concept connections and interact with them in a live network graph.
336
+ </p>
337
+ </div>
338
+ """)
339
+
340
+ with gr.Row():
341
+ with gr.Column(scale=1):
342
+ gr.Markdown("### 1. Upload Source Text")
343
+ with gr.Tabs():
344
+ with gr.TabItem("Paste Raw Text"):
345
+ text_input = gr.Textbox(
346
+ label="Source Text",
347
+ placeholder="Paste your text draft or chapter here to build a knowledge network...",
348
+ lines=12
349
+ )
350
+ with gr.TabItem("Upload Dataset File"):
351
+ file_input = gr.File(label="Upload (.csv, .xlsx, .txt)", file_types=[".csv", ".xlsx", ".txt"])
352
+ text_column_selector = gr.Dropdown(
353
+ label="Target Text Column",
354
+ choices=[],
355
+ visible=False,
356
+ interactive=True
357
+ )
358
+ status_text = gr.Markdown("No file uploaded yet.")
359
+
360
+ gr.Markdown("### 2. Configure Extraction")
361
+ method_selector = gr.Radio(
362
+ choices=["Local Noun-Chunk Parser (CPU & Fast)", "Transformers (AI Mode)"],
363
+ value="Local Noun-Chunk Parser (CPU & Fast)",
364
+ label="Extraction Parser"
365
+ )
366
+
367
+ with gr.Group() as token_group:
368
+ hf_token_input = gr.Textbox(
369
+ label="Hugging Face API Token",
370
+ placeholder="hf_...",
371
+ type="password",
372
+ visible=False,
373
+ info="Required to extract deep semantic relation triples. Get one free at huggingface.co."
374
+ )
375
+ hf_model_input = gr.Dropdown(
376
+ choices=[
377
+ "Qwen/Qwen2.5-7B-Instruct",
378
+ "meta-llama/Llama-3-8b-instruct"
379
+ ],
380
+ value="Qwen/Qwen2.5-7B-Instruct",
381
+ label="Transformer Model (HF API)",
382
+ visible=False
383
+ )
384
+
385
+ with gr.Row():
386
+ min_weight = gr.Slider(minimum=1, maximum=10, value=1, step=1, label="Min Co-occurrence Weight")
387
+ max_nodes = gr.Slider(minimum=5, maximum=40, value=20, step=1, label="Max Displayed Nodes")
388
+
389
+ run_btn = gr.Button("Build Knowledge Graph", variant="primary")
390
+
391
+ with gr.Column(scale=2):
392
+ gr.Markdown("### 3. Concept Knowledge Graph Visualization")
393
+ status_markdown = gr.Markdown("Enter text and click 'Build Knowledge Graph' to run.")
394
+
395
+ with gr.Tabs():
396
+ with gr.TabItem("Interactive Graph"):
397
+ chart_output = gr.Plot(label="Knowledge Graph Network")
398
+ with gr.TabItem("Nodes Table (Concepts)"):
399
+ nodes_table = gr.Dataframe(
400
+ headers=["Node", "Importance (Degree)"],
401
+ datatype=["str", "number"],
402
+ interactive=False
403
+ )
404
+ with gr.TabItem("Edges Table (Relationships)"):
405
+ edges_table = gr.Dataframe(
406
+ headers=["Source", "Target", "Relationship", "Weight"],
407
+ datatype=["str", "str", "str", "number"],
408
+ interactive=False
409
+ )
410
+
411
+ gr.Markdown("### 4. Export")
412
+ download_edges = gr.File(label="Download Concept Edges Table (CSV)")
413
+
414
+ # Show/hide token field depending on model
415
+ def toggle_method_fields(method):
416
+ if method == "Transformers (AI Mode)":
417
+ return gr.update(visible=True), gr.update(visible=True)
418
+ else:
419
+ return gr.update(visible=False), gr.update(visible=False)
420
+
421
+ method_selector.change(
422
+ fn=toggle_method_fields,
423
+ inputs=method_selector,
424
+ outputs=[hf_token_input, hf_model_input]
425
+ )
426
+
427
+ file_input.change(
428
+ fn=load_data,
429
+ inputs=file_input,
430
+ outputs=[df_state, text_column_selector, status_text]
431
+ )
432
+
433
+ run_btn.click(
434
+ fn=analyze_kg,
435
+ inputs=[text_input, file_input, text_column_selector, method_selector, hf_token_input, hf_model_input, min_weight, max_nodes],
436
+ outputs=[nodes_table, edges_table, chart_output, download_edges, status_markdown]
437
+ )
438
+
439
+ if __name__ == "__main__":
440
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ pandas
2
+ numpy
3
+ spacy
4
+ plotly
5
+ huggingface_hub
6
+ openpyxl