santanche commited on
Commit
964db98
·
1 Parent(s): e50fa8e

refactor (system): knowledge graph added

Browse files
README.md CHANGED
@@ -1,12 +1,12 @@
1
  ---
2
- title: Clinical Ner
3
- emoji: 🏆
4
  colorFrom: green
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: gpl-3.0
9
- short_description: Clinical NER
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Clinical NER / Knowledge Graph
3
+ emoji: 🕸️
4
  colorFrom: green
5
  colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: gpl-3.0
9
+ short_description: Clinical NER / Knowledge Graph
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app/clinical_ner.py CHANGED
@@ -215,3 +215,105 @@ class ClinicalNERProcessor:
215
 
216
  return "\n\n".join(sections)
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  return "\n\n".join(sections)
217
 
218
+ def generate_knowledge_graph(self, text, patient_id="patient_1"):
219
+ """
220
+ Generates a knowledge graph combining Clinical NER and Anatomy NER.
221
+
222
+ Args:
223
+ text (str): Input text to analyze
224
+ patient_id (str): Identifier for the patient node
225
+
226
+ Returns:
227
+ dict: Dictionary with 'nodes' and 'edges' lists
228
+ """
229
+ clinical_entities = self.basic_ner(text)
230
+ anatomy_entities = self.anatomy_ner(text)
231
+
232
+ nodes = []
233
+ edges = []
234
+ node_id = 0
235
+
236
+ # Add patient node
237
+ patient_node_id = node_id
238
+ nodes.append({
239
+ 'id': node_id,
240
+ 'label': patient_id,
241
+ 'type': 'patient'
242
+ })
243
+ node_id += 1
244
+
245
+ # Add entity type nodes
246
+ entity_types = ['problem', 'treatment', 'test']
247
+ type_node_ids = {}
248
+
249
+ for entity_type in entity_types:
250
+ type_node_ids[entity_type] = node_id
251
+ nodes.append({
252
+ 'id': node_id,
253
+ 'label': entity_type.capitalize(),
254
+ 'type': 'entity_type'
255
+ })
256
+ node_id += 1
257
+
258
+ # Add clinical entity nodes
259
+ for entity in clinical_entities:
260
+ entity_type = entity['entity_group'].lower()
261
+ entity_node_id = node_id
262
+
263
+ nodes.append({
264
+ 'id': node_id,
265
+ 'label': entity['word'],
266
+ 'type': entity_type,
267
+ 'score': entity['score']
268
+ })
269
+
270
+ # Connect entity to patient
271
+ edges.append({
272
+ 'source': patient_node_id,
273
+ 'target': entity_node_id,
274
+ 'label': 'has'
275
+ })
276
+
277
+ # Connect entity to entity type
278
+ if entity_type in type_node_ids:
279
+ edges.append({
280
+ 'source': entity_node_id,
281
+ 'target': type_node_ids[entity_type],
282
+ 'label': 'is_a'
283
+ })
284
+
285
+ node_id += 1
286
+
287
+ # Add Anatomy node
288
+ anatomy_node_id = node_id
289
+ nodes.append({
290
+ 'id': node_id,
291
+ 'label': 'Anatomy',
292
+ 'type': 'anatomy_category'
293
+ })
294
+ node_id += 1
295
+
296
+ # Add anatomy entity nodes
297
+ for entity in anatomy_entities:
298
+ entity_node_id = node_id
299
+
300
+ nodes.append({
301
+ 'id': node_id,
302
+ 'label': entity['word'],
303
+ 'type': 'anatomy',
304
+ 'score': entity['score']
305
+ })
306
+
307
+ # Connect anatomy entity to Anatomy node
308
+ edges.append({
309
+ 'source': entity_node_id,
310
+ 'target': anatomy_node_id,
311
+ 'label': 'is_a'
312
+ })
313
+
314
+ node_id += 1
315
+
316
+ return {
317
+ 'nodes': nodes,
318
+ 'edges': edges
319
+ }
app/server_clinical_ner.py CHANGED
@@ -93,6 +93,23 @@ class PrologCombinedResponse(BaseModel):
93
  anatomy_count: int
94
  token_count: int
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  @app.get("/")
97
  async def root():
98
  return RedirectResponse(url="/browser/")
@@ -285,6 +302,33 @@ async def health_check():
285
  "anatomy_available": ner_model.anatomy_pipeline is not None if ner_model else False
286
  }
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  @app.get("/models/info")
289
  async def models_info():
290
  """Get information about loaded models"""
 
93
  anatomy_count: int
94
  token_count: int
95
 
96
+ class KnowledgeGraphNode(BaseModel):
97
+ id: int
98
+ label: str
99
+ type: str
100
+ score: float | None = None
101
+
102
+ class KnowledgeGraphEdge(BaseModel):
103
+ source: int
104
+ target: int
105
+ label: str
106
+
107
+ class KnowledgeGraphResponse(BaseModel):
108
+ nodes: list[KnowledgeGraphNode]
109
+ edges: list[KnowledgeGraphEdge]
110
+ node_count: int
111
+ edge_count: int
112
+
113
  @app.get("/")
114
  async def root():
115
  return RedirectResponse(url="/browser/")
 
302
  "anatomy_available": ner_model.anatomy_pipeline is not None if ner_model else False
303
  }
304
 
305
+ @app.post("/knowledge-graph", response_model=KnowledgeGraphResponse)
306
+ async def generate_knowledge_graph(request: TextRequest):
307
+ """
308
+ Generate a knowledge graph combining Clinical NER and Anatomy NER.
309
+
310
+ Returns nodes and edges representing:
311
+ - Patient node connected to clinical entities
312
+ - Entity type nodes (Problem, Treatment, Test)
313
+ - Clinical entities connected to their types
314
+ - Anatomy category node
315
+ - Anatomical entities connected to Anatomy node
316
+ """
317
+ try:
318
+ if not request.text.strip():
319
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
320
+
321
+ graph = ner_model.generate_knowledge_graph(request.text)
322
+
323
+ return {
324
+ "nodes": graph['nodes'],
325
+ "edges": graph['edges'],
326
+ "node_count": len(graph['nodes']),
327
+ "edge_count": len(graph['edges'])
328
+ }
329
+ except Exception as e:
330
+ raise HTTPException(status_code=500, detail=f"Error processing text: {str(e)}")
331
+
332
  @app.get("/models/info")
333
  async def models_info():
334
  """Get information about loaded models"""
app/static/browser/index.html CHANGED
@@ -474,6 +474,7 @@
474
  <button id="anatomyBtn" class="btn btn-anatomy">🫀 Anatomy NER</button>
475
  <button id="posBtn" class="btn btn-secondary">📝 POS Tagging</button>
476
  <button id="combinedBtn" class="btn btn-success">🎯 Combined Analysis</button>
 
477
  </div>
478
  </div>
479
 
@@ -490,6 +491,7 @@
490
  <button class="tab" data-tab="anatomy">Anatomy NER/Visualization</button>
491
  <button class="tab" data-tab="pos">POS Results</button>
492
  <button class="tab" data-tab="combined">Combined</button>
 
493
  </div>
494
 
495
  <div id="ner" class="tab-content active">
@@ -606,6 +608,44 @@
606
  <pre id="prologFactsCombined" class="prolog-output"></pre>
607
  </div>
608
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
  </div>
610
  </div>
611
  </div>
@@ -868,6 +908,132 @@
868
  }
869
  });
870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
  function displayNERResults(originalText, entities, prolog) {
872
  const sortedEntities = [...entities].sort((a, b) => a.start - b.start);
873
 
 
474
  <button id="anatomyBtn" class="btn btn-anatomy">🫀 Anatomy NER</button>
475
  <button id="posBtn" class="btn btn-secondary">📝 POS Tagging</button>
476
  <button id="combinedBtn" class="btn btn-success">🎯 Combined Analysis</button>
477
+ <button id="graphBtn" class="btn" style="background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); color: #333;">🕸️ Knowledge Graph</button>
478
  </div>
479
  </div>
480
 
 
491
  <button class="tab" data-tab="anatomy">Anatomy NER/Visualization</button>
492
  <button class="tab" data-tab="pos">POS Results</button>
493
  <button class="tab" data-tab="combined">Combined</button>
494
+ <button class="tab" data-tab="graph">Knowledge Graph</button>
495
  </div>
496
 
497
  <div id="ner" class="tab-content active">
 
608
  <pre id="prologFactsCombined" class="prolog-output"></pre>
609
  </div>
610
  </div>
611
+
612
+ <div id="graph" class="tab-content">
613
+ <div class="result-section">
614
+ <h2>🕸️ Knowledge Graph Visualization</h2>
615
+ <div id="graph-container"></div>
616
+ </div>
617
+
618
+ <div class="graph-tables">
619
+ <div class="graph-table">
620
+ <h3>📊 Nodes Table</h3>
621
+ <table id="nodesTable">
622
+ <thead>
623
+ <tr>
624
+ <th>ID</th>
625
+ <th>Label</th>
626
+ <th>Type</th>
627
+ <th>Score</th>
628
+ </tr>
629
+ </thead>
630
+ <tbody></tbody>
631
+ </table>
632
+ </div>
633
+
634
+ <div class="graph-table">
635
+ <h3>🔗 Edges Table</h3>
636
+ <table id="edgesTable">
637
+ <thead>
638
+ <tr>
639
+ <th>Source</th>
640
+ <th>Target</th>
641
+ <th>Relationship</th>
642
+ </tr>
643
+ </thead>
644
+ <tbody></tbody>
645
+ </table>
646
+ </div>
647
+ </div>
648
+ </div>
649
  </div>
650
  </div>
651
  </div>
 
908
  }
909
  });
910
 
911
+ // Knowledge Graph button handler
912
+ graphBtn.addEventListener('click', async () => {
913
+ const text = clinicalText.value.trim();
914
+
915
+ if (!text) {
916
+ showError('Please enter some text to analyze.');
917
+ return;
918
+ }
919
+
920
+ hideError();
921
+ hideResults();
922
+ showLoading();
923
+ graphBtn.disabled = true;
924
+
925
+ try {
926
+ const response = await fetch(`${API_BASE_URL}/knowledge-graph`, {
927
+ method: 'POST',
928
+ headers: { 'Content-Type': 'application/json' },
929
+ body: JSON.stringify({ text })
930
+ });
931
+
932
+ if (!response.ok) {
933
+ throw new Error('API request failed');
934
+ }
935
+
936
+ const data = await response.json();
937
+ displayKnowledgeGraph(data.nodes, data.edges);
938
+ switchToTab('graph');
939
+ showResults();
940
+ } catch (err) {
941
+ showError(`Error: ${err.message}. Please check if the API service is running.`);
942
+ } finally {
943
+ hideLoading();
944
+ graphBtn.disabled = false;
945
+ }
946
+ });
947
+
948
+ function displayKnowledgeGraph(nodes, edges) {
949
+ // Display tables
950
+ const nodesTableBody = document.querySelector('#nodesTable tbody');
951
+ nodesTableBody.innerHTML = '';
952
+ nodes.forEach(node => {
953
+ const row = nodesTableBody.insertRow();
954
+ row.innerHTML = `
955
+ <td>${node.id}</td>
956
+ <td class="node-${node.type}">${node.label}</td>
957
+ <td>${node.type}</td>
958
+ <td>${node.score ? node.score.toFixed(4) : '-'}</td>
959
+ `;
960
+ });
961
+
962
+ const edgesTableBody = document.querySelector('#edgesTable tbody');
963
+ edgesTableBody.innerHTML = '';
964
+ edges.forEach(edge => {
965
+ const sourceNode = nodes.find(n => n.id === edge.source);
966
+ const targetNode = nodes.find(n => n.id === edge.target);
967
+ const row = edgesTableBody.insertRow();
968
+ row.innerHTML = `
969
+ <td>${sourceNode ? sourceNode.label : edge.source}</td>
970
+ <td>${targetNode ? targetNode.label : edge.target}</td>
971
+ <td>${edge.label}</td>
972
+ `;
973
+ });
974
+
975
+ // Visualize graph
976
+ const container = document.getElementById('graph-container');
977
+
978
+ // Define colors for different node types
979
+ const nodeColors = {
980
+ patient: { background: '#e74c3c', border: '#c0392b' },
981
+ entity_type: { background: '#3498db', border: '#2980b9' },
982
+ problem: { background: '#ffcccb', border: '#8b0000' },
983
+ treatment: { background: '#c7f0c7', border: '#006400' },
984
+ test: { background: '#cce5ff', border: '#004085' },
985
+ anatomy_category: { background: '#9b59b6', border: '#8e44ad' },
986
+ anatomy: { background: '#e8daef', border: '#9b59b6' }
987
+ };
988
+
989
+ // Prepare nodes for vis.js
990
+ const visNodes = nodes.map(node => ({
991
+ id: node.id,
992
+ label: node.label,
993
+ color: nodeColors[node.type] || { background: '#95a5a6', border: '#7f8c8d' },
994
+ font: { color: '#333', size: 14 },
995
+ shape: node.type === 'patient' ? 'diamond' :
996
+ (node.type === 'entity_type' || node.type === 'anatomy_category') ? 'box' : 'ellipse'
997
+ }));
998
+
999
+ // Prepare edges for vis.js
1000
+ const visEdges = edges.map(edge => ({
1001
+ from: edge.source,
1002
+ to: edge.target,
1003
+ label: edge.label,
1004
+ arrows: 'to',
1005
+ font: { size: 10, align: 'middle' }
1006
+ }));
1007
+
1008
+ // Create network
1009
+ const data = {
1010
+ nodes: new vis.DataSet(visNodes),
1011
+ edges: new vis.DataSet(visEdges)
1012
+ };
1013
+
1014
+ const options = {
1015
+ layout: {
1016
+ hierarchical: {
1017
+ enabled: true,
1018
+ direction: 'UD',
1019
+ sortMethod: 'directed',
1020
+ levelSeparation: 150,
1021
+ nodeSpacing: 200
1022
+ }
1023
+ },
1024
+ physics: {
1025
+ enabled: false
1026
+ },
1027
+ interaction: {
1028
+ dragNodes: true,
1029
+ dragView: true,
1030
+ zoomView: true
1031
+ }
1032
+ };
1033
+
1034
+ new vis.Network(container, data, options);
1035
+ }
1036
+
1037
  function displayNERResults(originalText, entities, prolog) {
1038
  const sortedEntities = [...entities].sort((a, b) => a.start - b.start);
1039