rafmacalaba commited on
Commit
a0234ff
Β·
1 Parent(s): bab3a78
Files changed (2) hide show
  1. app.py +462 -284
  2. requirements.txt +1 -7
app.py CHANGED
@@ -1,4 +1,10 @@
 
1
  import gradio as gr
 
 
 
 
 
2
  import os
3
  import subprocess
4
 
@@ -12,318 +18,491 @@ if GH_TOKEN:
12
  check=True
13
  )
14
  else:
15
- print("⚠️ GH_TOKEN not found. Private ai4data_use will NOT be installed.")
16
 
17
- from ai4data import extract_from_text
18
  DATA_MODEL_ID = "rafmacalaba/datause-extraction-v3-finetuned"
19
- DATASET_COLORS = {
20
- "named": "#ff6b6b",
21
- "unnamed": "#51cf66",
22
- "vague": "#fcc419",
23
- "invalid": "#868e96"
24
- }
25
-
26
- RELATION_COLORS = {
27
- "acronym": "#4dabf7", # REL_ACR
28
- "author": "#f06595", # REL_AUT
29
- "data description": "#e599f7", # REL_DSC
30
- "data geography": "#20c997", # REL_GEO
31
- "data type": "#339af0", # REL_TYP
32
- "publication year": "#f783ac", # REL_PY
33
- "publisher": "#4dabf7", # REL_PUB
34
- "reference population": "#8e44ad", # REL_POP
35
- "reference year": "#ffd43b", # REL_RY
36
- # Removed on purpose β€” we are filtering these out
37
- # "usage context": "#e8590c"
38
- }
39
-
40
- RELATION_DISPLAY = {
41
- "data geography": "geography",
42
- "data description": "description",
43
- }
44
-
45
-
46
- def overlaps(a, b):
47
- # spans overlap if they share any character position
48
- return not (a["end"] <= b["start"] or a["start"] >= b["end"])
49
-
50
-
51
- # def collect_spans(res):
52
- base = res[0]["text"]
53
-
54
- dataset_spans = []
55
- relation_spans = []
56
- usage_context_map = {}
57
- # --- Collect DATASETS (always keep) ---
58
- for item in res:
59
  ds = item.get("datasets")
60
  if ds:
61
- dataset_spans.append({
62
- "start": ds["start"],
63
- "end": ds["end"],
64
- "label": ds["label"].lower(),
65
- "kind": "dataset",
66
- "text": base[ds["start"]:ds["end"]]
67
- })
68
-
69
- # --- Collect RELATIONS (but skip usage context) ---
70
- for item in res:
71
- for r in item.get("relations", []):
72
- if r["relation"].lower() == "usage context": # remove usage context entirely
 
 
 
 
73
  continue
74
- if r.get("score", 0) < 0.6:
75
- continue # ignore low-confidence relations
76
- relation_spans.append({
77
- "start": r["start"],
78
- "end": r["end"],
79
- "label": r["relation"].lower(),
80
- "kind": "relation",
81
- "text": base[r["start"]:r["end"]],
82
- "source": r['source']
83
- })
84
-
85
- # --- Priority Rule: Dataset spans win over relation overlaps ---
86
- def overlaps(a, b):
87
- return not (a["end"] <= b["start"] or a["start"] >= b["end"])
88
-
89
- dataset_spans = sorted(dataset_spans, key=lambda s: s["start"])
90
-
91
- filtered_relations = []
92
- for rel in relation_spans:
93
- if any(overlaps(rel, ds) for ds in dataset_spans):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  continue
95
- filtered_relations.append(rel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # --- Deduplicate spans ---
98
- uniq = {}
99
- for span in dataset_spans + filtered_relations:
100
- key = (span["start"], span["end"], span["label"], span["kind"])
101
- uniq[key] = span # last wins (order doesn't matter because datasets sorted first)
102
 
103
- spans = list(uniq.values())
104
- spans = sorted(spans, key=lambda s: s["start"])
 
 
105
 
106
- return spans
 
 
107
 
108
- def collect_spans(res):
109
- base = res[0]["text"]
 
 
110
 
111
- dataset_spans = []
112
- relation_spans = []
 
113
 
114
- # We store usage context *indexed by item*, not by string
115
- usage_context_for_item = {}
 
116
 
117
- # --- First: capture usage context by item index ---
118
- for idx, item in enumerate(res):
119
- for r in item.get("relations", []):
120
- if r["relation"].lower() == "usage context":
121
- usage_context_for_item[idx] = r.get("target", "").lower()
122
 
123
- # --- Collect DATASETS and attach usage context correctly ---
124
- for idx, item in enumerate(res):
125
- ds = item.get("datasets")
126
- if ds:
127
- usage = usage_context_for_item.get(idx, None)
128
- dataset_spans.append({
129
- "start": ds["start"],
130
- "end": ds["end"],
131
- "label": ds["label"].lower(),
132
- "kind": "dataset",
133
- "text": base[ds["start"]:ds["end"]],
134
- "usage": usage # βœ… always aligned β€” no string matching needed
135
- })
136
-
137
- # --- Collect RELATIONS (excluding usage context & low confidence) ---
138
- for idx, item in enumerate(res):
139
- for r in item.get("relations", []):
140
- rel_label = r["relation"].lower()
141
- if rel_label == "usage context":
142
- continue
143
- if r.get("score", 0) < 0.6:
144
  continue
145
- relation_spans.append({
146
- "start": r["start"],
147
- "end": r["end"],
148
- "label": rel_label,
149
- "kind": "relation",
150
- "text": base[r["start"]:r["end"]],
151
- "source": r['source'],
152
- "source_item": idx # align with dataset via index
153
- })
154
-
155
- # --- Priority Rule: Dataset spans win over relation overlaps ---
156
- def overlaps(a, b):
157
- return not (a["end"] <= b["start"] or a["start"] >= b["end"])
158
-
159
- dataset_spans = sorted(dataset_spans, key=lambda s: s["start"])
160
-
161
- filtered_relations = []
162
- for rel in relation_spans:
163
- if any(overlaps(rel, ds) for ds in dataset_spans):
164
- continue
165
- filtered_relations.append(rel)
166
-
167
- # --- Deduplicate ---
168
- uniq = {(s["start"], s["end"], s["label"], s["kind"]): s
169
- for s in dataset_spans + filtered_relations}
170
-
171
- spans = sorted(uniq.values(), key=lambda s: s["start"])
172
- return spans
173
-
174
-
175
- def render_highlight(text):
176
- res = extract_from_text(text, use_classifier_gate=False)
177
- base = res[0]["text"]
178
-
179
- spans = collect_spans(res)
180
-
181
- if not spans:
182
- return f"<div style='white-space:pre-wrap;'>{base}</div>"
183
-
184
- html = """<style>
185
- .hl {
186
- position: relative;
187
- cursor: pointer;
188
- display: inline-block;
189
- }
190
-
191
- .hl:hover::after {
192
- content: attr(data-tip);
193
- position: absolute;
194
- top: -2.2em;
195
- left: 0;
196
- background: #fff; /* white tooltip */
197
- color: #222; /* dark text */
198
- padding: 4px 8px;
199
- font-size: 0.78em;
200
- border-radius: 6px;
201
- border: 1px solid #ccc; /* subtle border */
202
- box-shadow: 0px 2px 6px rgba(0,0,0,0.15); /* soft shadow */
203
- white-space: nowrap;
204
- z-index: 9999;
205
- opacity: 1;
206
- }
207
-
208
- .hl::after {
209
- opacity: 0;
210
- pointer-events: none;
211
- transition: opacity 0.05s; /* still instant, but fades cleanly */
212
- }
213
- </style>
214
- <div style='white-space:pre-wrap; font-family:monospace;'>
215
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
 
 
217
 
218
- last = 0
219
-
220
- for span in spans:
221
- html += base[last:span["start"]]
222
-
223
- if span["kind"] == "dataset":
224
- color = DATASET_COLORS.get(span["label"], "#999")
225
- else:
226
- color = RELATION_COLORS.get(span["label"], "#74c0fc")
227
-
228
- # if span["kind"] == "dataset":
229
- # tooltip = span["label"]
230
- if span["kind"] == "dataset":
231
- tooltip = span["label"] if not span.get("usage") else f"usage context: {span['usage']}"
232
-
233
- else:
234
- # Show which dataset this relation corresponds to
235
- # We look up the nearest dataset with same extraction item.
236
- # But since source is provided already, use it directly:
237
- tooltip = f"{span['label']} for {span['source']}"
238
-
239
- display_label = RELATION_DISPLAY.get(span["label"], span["label"])
240
- html += (
241
- f"<span class='hl' data-tip='{tooltip}' "
242
- f"style='background:{color}22; border:1px solid {color}; "
243
- f"border-radius:4px; padding:0px 3px; margin:0 2px;'>"
244
- f"{span['text']}"
245
- f"<span style='border:1px solid {color}; color:{color}; "
246
- f"font-size:0.7em; border-radius:3px; padding:1px 3px; margin-left:5px;'>"
247
- f"{display_label}</span></span>"
248
  )
249
- last = span["end"]
250
-
251
- html += base[last:]
252
- html += "</div>"
253
-
254
- return html
255
-
256
- with gr.Blocks(
257
- title="AI for Data Use: Dataset Extraction",
258
- css="""
259
- body, .gradio-container { background-color: #111 !important; color: #e6e6e6 !important; }
260
-
261
- textarea, input, .gr-textbox {
262
- background: #1a1a1a !important;
263
- color: #f2f2f2 !important;
264
- border: 1px solid #444 !important;
265
- border-radius: 6px !important;
266
- }
267
- textarea:focus, input:focus {
268
- border-color: #888 !important;
269
- box-shadow: 0 0 0 1px #888 !important;
270
- }
271
-
272
- button {
273
- background: #333 !important;
274
- color: #eee !important;
275
- border-radius: 6px !important;
276
- border: 1px solid #444 !important;
277
- padding: 8px 14px !important;
278
- }
279
- button:hover {
280
- background: #444 !important;
281
- border-color: #666 !important;
282
- }
283
-
284
- .gr-html {
285
- background: #1a1a1a !important;
286
- color: #fff !important;
287
- border-radius: 6px !important;
288
- border: 1px solid #333 !important;
289
- padding: 14px !important;
290
- min-height: 200px;
291
- }
292
- """
293
- ) as demo:
294
 
295
- gr.Markdown("# AI for Data Use: Dataset Extraction")
 
 
 
 
296
 
297
- with gr.Row():
298
- with gr.Column(scale=1):
299
- input_box = gr.Textbox(lines=14, label="Input Text")
 
300
 
301
- with gr.Column(scale=1):
302
- output_box = gr.HTML(
303
- label="Highlighted Output",
304
- value="<div style='min-height:200px; border:1px solid #444; border-radius:8px; padding:10px; opacity:0.6; text-align:center;'>Waiting for input…</div>"
 
 
 
305
  )
306
 
307
- submit_btn = gr.Button("Submit")
308
 
309
- submit_btn.click(
310
- fn=render_highlight,
311
- inputs=input_box,
312
- outputs=output_box
 
 
 
 
 
 
 
313
  )
314
 
 
 
 
 
 
 
 
 
 
315
  gr.Examples(
316
- examples=[
317
- ["We examine early childhood nutrition and health inequality across Sub-Saharan Africa by combining information from the Demographic and Health Surveys (DHS, various years) and the Multiple Indicator Cluster Surveys (MICS). Anthropometric outcomes (height-for-age, weight-for-age) are standardized using WHO Child Growth Standards. To account for environmental exposure, we align survey cluster coordinates with gridded temperature and precipitation data from the CRU TS 4.06 dataset at the nearest spatial cell."],
318
- ["Introduction The mining sector in Africa is growing rapidly and is the main recipient of foreign direct investment ( World Bank 2011 ). The welfare effects of this sector are not well understood, although a literature has recently developed around this question. The main contribution of this paper is to shed light on the welfare effects of gold mining in a detailed, in-depth country study of Ghana, a country with a long tradition of gold mining and a recent, large expansion in capital - intensive and industrial-scale production. A second contribution of this paper is to show the importance of decomposing the effects with respect to distance from the mines. Given the spatial heterogeneity of the results, we explore the effects in an individual-level, difference-in-differences analysis by using spatial lag models to allow for nonlinear effects with distance from mine. We also allow for spillovers across districts, in a district-level analysis. We use two complementary geocoded household data sets to analyze outcomes in Ghana: the Demographic and Health Survey ( DHS ) and the Ghana Living Standard Survey ( GLSS ), which provide information on a wide range of welfare outcomes."],
319
- ["The main mining data is a dataset from InterraRMG covering all large-scale mines in Ghana, explained in more detail in section 3. 1. This dataset is linked to survey data from the DHS and GLSS, using spatial information. Geographical coordinates of enumeration areas in GLSS are from Ghana Statistical Services ( GSS ). 2 Point coordinates ( global positioning system [ GPS ] ) for the surveyed DHS clusters3 allow us to match all individuals to one or several mineral mines. We do this in two ways. First, we calculate distance spans from an exact mine location given by its GPS coordinates, and match surveyed individuals to mines. "],
320
- ["We study learning outcomes by linking standardized test score data from the Programme for International Student Assessment (PISA) and the Trends in International Mathematics and Science Study (TIMSS) to school resource indicators compiled by the UNESCO Institute for Statistics (UIS). Household socioeconomic background is proxied using parental education information extracted from the Demographic and Health Surveys (DHS) female and household datasets. To examine the role of inequality, we calculate within-school and between-school variance components and correlate these with school funding data from national Ministry of Education financial reports. The goal is to understand how unequal resource allocation contributes to learning gaps across income groups and geographic regions."],
321
- ["Patterns of forced displacement are analyzed using the UNHCR Refugee Population Statistics Database, which provides annual country-to-country asylum flows and refugee stock counts. To understand displacement drivers, we integrate conflict event data from the ACLED conflict monitoring system and national political stability indices from the World Governance Indicators (WGI). We additionally incorporate bilateral migration dyads from the World Bank Global Bilateral Migration Database to capture historical migration ties. Geographic exposure to violence is assigned using region-level coordinates and spatial joining procedures. This dataset allows us to estimate the relationship between escalating conflict intensity and subsequent cross-border population movements."],
322
- ],
323
- inputs=input_box
324
  )
325
 
326
- gr.Markdown("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  <hr style='border: none; border-top: 1px solid #333; margin: 20px 0;'>
328
 
329
  ### AI for Data Use: Dataset Extraction
@@ -358,6 +537,5 @@ The detected spans are **highlighted inline** with labels so you can quickly rev
358
  - Project Docs: https://worldbank.github.io/ai4data-use/docs/introduction.html
359
  """)
360
 
361
- demo.launch()
362
-
363
 
 
 
1
+ import re
2
  import gradio as gr
3
+ from collections import defaultdict
4
+ from ai4data import extract_from_text, deduplicate_mentions
5
+ import html
6
+ from typing import List
7
+
8
  import os
9
  import subprocess
10
 
 
18
  check=True
19
  )
20
  else:
21
+ print("GH_TOKEN not found. Private ai4data_use will NOT be installed.")
22
 
 
23
  DATA_MODEL_ID = "rafmacalaba/datause-extraction-v3-finetuned"
24
+
25
+ def tokenize_text(text: str) -> List[str]:
26
+ """Tokenize the input text into a list of tokens (words, punctuation)."""
27
+ return re.findall(r'\w+(?:[-_]\w+)*|\S', text)
28
+
29
+ def truncate_text(text: str, max_tokens: int = 300) -> str:
30
+ """
31
+ Tokenize the text and truncate to the first `max_tokens` tokens,
32
+ rejoining them into a valid string.
33
+ """
34
+ tokens = tokenize_text(text)
35
+ if len(tokens) <= max_tokens:
36
+ return text.strip()
37
+ truncated = tokens[:max_tokens]
38
+ return " ".join(truncated).strip() + " ..."
39
+
40
+ def process_text(text, distance_threshold=1000, short_threshold=100, debug=False, precomputed_results=None):
41
+ if not text.strip():
42
+ return [], []
43
+
44
+ # Use precomputed results if provided
45
+ results = precomputed_results if precomputed_results is not None else extract_from_text(text)
46
+ if not results:
47
+ return [], []
48
+
49
+ base_text = results[0]["text"].strip()
50
+ from collections import defaultdict
51
+ import re
52
+
53
+ entity_map = defaultdict(list)
54
+ dataset_spans = [] # (start, end, text, label)
55
+ all_relations = []
56
+ absorbed_vague_datasets = set()
57
+
58
+ # --- 1️⃣ Collect dataset spans ---
59
+ for item in results:
 
 
 
 
60
  ds = item.get("datasets")
61
  if ds:
62
+ start, end = ds["start"], ds["end"]
63
+ ds_label = ds["label"]
64
+ ds_text = ds["text"]
65
+ dataset_spans.append((start, end, ds_text, ds_label))
66
+
67
+ # --- 2️⃣ Collect relations (keep for tree even if overlapping) ---
68
+ seen_descriptions = set()
69
+ for item in results:
70
+ for rel in item.get("relations", []):
71
+ start, end = rel["start"], rel["end"]
72
+ src = rel["source"]
73
+ relation = rel["relation"].strip().lower()
74
+ target_text = base_text[start:end].strip()
75
+
76
+ # Skip implausible year relations
77
+ if relation in {"reference year", "publication year"} and not re.search(r"\b\d{4}\b", target_text):
78
  continue
79
+
80
+ limit = short_threshold if relation in {"data description", "data type"} else distance_threshold
81
+ linked_datasets = []
82
+
83
+ # --- Find nearby datasets within threshold ---
84
+ if dataset_spans:
85
+ for ds_start, ds_end, ds_text, _ in dataset_spans:
86
+ dist = abs(ds_start - start)
87
+ if dist <= limit:
88
+ linked_datasets.append(ds_text)
89
+
90
+ # --- If none found, apply shared-relation logic ---
91
+ if not linked_datasets:
92
+ # Shared or global relations β€” always apply to all datasets
93
+ if relation in {"publisher", "reference year", "publication year", "usage context"} and dataset_spans:
94
+ linked_datasets = [d[2] for d in dataset_spans]
95
+ else:
96
+ continue # skip too-distant or unrelated relation
97
+
98
+ # --- Supersession rule: vague dataset near named one ---
99
+ if relation in {"data type"}:
100
+ for ds_start, ds_end, ds_text, ds_label in dataset_spans:
101
+ # only absorb if relation's source explicitly mentions this dataset
102
+ if ds_label == "vague" and ds_text in rel["source"] and abs(ds_start - start) <= short_threshold:
103
+ absorbed_vague_datasets.add(ds_text)
104
+
105
+ # --- Deduplication for data description ---
106
+ if relation == "data description":
107
+ key = target_text.lower()
108
+ if key in seen_descriptions:
109
+ continue
110
+ seen_descriptions.add(key)
111
+
112
+ # --- Store relation for all linked datasets ---
113
+ for ds_text in linked_datasets:
114
+ relation_entry = {
115
+ "start": start,
116
+ "end": end,
117
+ "relation": relation,
118
+ "src": ds_text,
119
+ }
120
+
121
+ # only store text value for usage context
122
+ if relation == "usage context":
123
+ relation_entry["value"] = target_text or ""
124
+
125
+ all_relations.append(relation_entry)
126
+ if debug:
127
+ print(f"Linked {relation} β†’ {ds_text}")
128
+
129
+ # --- 3️⃣ Add dataset highlights (priority layer) ---
130
+ for ds_start, ds_end, ds_text, ds_label in dataset_spans:
131
+ if ds_label == "vague" and ds_text in absorbed_vague_datasets:
132
  continue
133
+ entity_map[(ds_start, ds_end)].append(f"dataset:{ds_label}")
134
+
135
+ # --- 4️⃣ Add relation spans, skip those overlapping datasets ---
136
+ relation_priority = [
137
+ "acronym", "publisher", "data type", "data description",
138
+ "reference year", "publication year", "data geography", "usage context"
139
+ ]
140
+
141
+ relation_map = defaultdict(list)
142
+ for rel in all_relations:
143
+ key = (rel["start"], rel["end"])
144
+ relation_map[key].append(rel["relation"])
145
+
146
+ for (start, end), rels in relation_map.items():
147
+ # --- improved overlap: skip if relation is fully or partially inside a dataset span ---
148
+ overlaps_dataset = False
149
+ for ds_start, ds_end, _, _ in dataset_spans:
150
+ if (start >= ds_start and end <= ds_end) or (start < ds_end and end > ds_start):
151
+ overlaps_dataset = True
152
+ break
153
+
154
+ if overlaps_dataset:
155
+ continue # keep for tree, not highlight
156
+
157
+ # choose top relation if multiple overlap on same span
158
+ if len(rels) > 1:
159
+ rels.sort(key=lambda r: relation_priority.index(r) if r in relation_priority else len(relation_priority))
160
+ top_relation = rels[0]
161
+ entity_map[(start, end)].append(top_relation)
162
+
163
+ # --- 5️⃣ Rebuild final text output for Gradio highlight ---
164
+ sorted_spans = sorted(entity_map.items(), key=lambda x: x[0][0])
165
+ output = []
166
+ last_idx = 0
167
+ for (start, end), labels in sorted_spans:
168
+ if start > last_idx:
169
+ output.append((base_text[last_idx:start], None))
170
+ substring = base_text[start:end]
171
+ label = ", ".join(labels)
172
+ output.append((substring, label))
173
+ last_idx = end
174
+
175
+ if last_idx < len(base_text):
176
+ output.append((base_text[last_idx:], None))
177
+
178
+ return output, results
179
+
180
+
181
+
182
+ # --- 🌍 Long, realistic research examples ---
183
+ example_texts = [
184
+ # 1️⃣ Ghana mining + household surveys
185
+ """Introduction The mining sector in Africa is growing rapidly and remains the main recipient of foreign direct investment ( World Bank 2011 ). The welfare effects of this sector are not well understood, although a literature has recently developed around this question. The main contribution of this paper is to shed light on the welfare effects of gold mining in a detailed, in-depth country study of Ghana, a country with a long tradition of gold mining and a recent, large expansion in capital-intensive and industrial-scale production. We use two complementary geocoded household data sets to analyze outcomes in Ghana: the Demographic and Health Survey ( DHS ) and the Ghana Living Standard Survey ( GLSS ), both of which provide information on household welfare, education, and demographic outcomes. The empirical analysis relies on district-level spatial models and difference-in-differences estimation to capture local spillover effects. We also examine how outcomes vary by proximity to large mines and across survey years from 2005 to 2015, controlling for baseline characteristics and mining expansion phases.""",
186
+
187
+ # 2️⃣ WDI, IEA, and energy transition
188
+ """This study analyzes labor and household data in the context of Afghan refugees in Iran. The national household surveys in Iran have good coverage of them.
189
+ The Labor Force Survey (LFS) has been conducted in the middle of each quarter since spring 2005 and is the primary source of employment statistics in Iran.
190
+ The LFS covers a wide range of topics including household members’ demographic and employment status, such as education, migration, working hours, industry, occupation, and experience (but not wage and income).
191
+ Importantly for this research, the nationality of each household member is also inquired in this survey. The sampling of LFS is on a rotating panel basis in the sense that each household is sampled in two consecutive seasons of two consecutive years.
192
+ This feature enables us to observe each individual’s change in employment status and compare it between two communities. Table 1 lists the LFS rounds with the available sample of Afghan refugees.
193
+ HEIS (Household Expenditure and Income Survey) has been conducted annually since 1963, but its raw data is available from 1984 onwards.""",
194
+
195
+ # 3️⃣ DHS and MICS in maternal and child health
196
+ """Observations are also spatially identified at the municipality level, but here we focus on variation in the Venezuelan share of the population at the province level, of which there are 196, as these are best representative of local labor markets. The Latin American Public Opinion Project (LAPOP) is an opinion survey conducted bi-annually in all countries in Latin America and designed to be representative of urban populations. This was fielded in Peru in 2010, 2012, 2014, 2017 and 2019 and consists of about 2,000 observations from mostly urban areas."""
197
+ ,
198
+
199
+ # 4️⃣ UNHCR, ProGres, and displacement data
200
+ """Such null effects of refugee migration on native attitudes in host communities are also identified by Zhou et al. (2021) for the case of Sudanese refugee immigration to Uganda. Our analysis relies on data from the following sources: the Encuesta Dirigida a la PoblaciΓ³n Venezolana que Reside en el PaΓ­s (ENPOVE), which is a specialized survey of Venezuelans living in Peru conducted by the National Institute of Statistics (INEI) in December 2018. The survey covers five main urban areas in the country where Venezuelan immigrants were most likely to be present.""",
201
+ ]
202
+
203
+ def make_tree_from_highlight(highlight_output, max_distance=250):
204
+ """
205
+ Build dataset–relation tree using simplified labels from process_text().
206
+ Proximity-based grouping: attach relations to nearest dataset mention.
207
+ """
208
+ if not highlight_output:
209
+ return "<i>No relations found.</i>"
210
+
211
+ # Collect dataset mentions and relation mentions
212
+ datasets = []
213
+ relations = []
214
+ for i, (segment, label) in enumerate(highlight_output):
215
+ if not label:
216
+ continue
217
+ labels = [lbl.strip() for lbl in label.split(",")]
218
+ for lbl in labels:
219
+ if lbl.startswith("dataset:"):
220
+ datasets.append({
221
+ "idx": i,
222
+ "name": segment.strip(),
223
+ "label": lbl.split(":")[1].strip()
224
+ })
225
+ elif lbl in {
226
+ "acronym", "publisher", "data type", "data description",
227
+ "reference year", "publication year", "data geography", "usage context"
228
+ }:
229
+ # βœ… Store 'target' and 'value' separately β€” only for usage context
230
+ rel_entry = {"idx": i, "type": lbl, "target": segment.strip()}
231
+ if lbl == "usage context":
232
+ rel_entry["value"] = segment.strip() # target text = value
233
+ relations.append(rel_entry)
234
+
235
+ if not datasets and not relations:
236
+ return "<i>No relations found.</i>"
237
+
238
+ # Build a dict to hold dataset β†’ {relation_type β†’ [targets]}
239
+ tree = {ds["name"]: {"_dataset_label": ds["label"]} for ds in datasets}
240
+
241
+ # Attach relations to nearest dataset mention (proximity-based)
242
+ for rel in relations:
243
+ if not datasets:
244
+ continue
245
+ nearest = min(datasets, key=lambda ds: abs(ds["idx"] - rel["idx"]))
246
+ if abs(nearest["idx"] - rel["idx"]) <= max_distance:
247
+ tree.setdefault(nearest["name"], {"_dataset_label": nearest["label"]})
248
+ rel_type = rel["type"]
249
+ # βœ… store 'value' separately for usage context
250
+ if rel_type == "usage context":
251
+ tree[nearest["name"]].setdefault(rel_type, set()).add(rel.get("value", ""))
252
+ else:
253
+ tree[nearest["name"]].setdefault(rel_type, set()).add(rel["target"])
254
+
255
+ # --- Render HTML ---
256
+ html_out = ["<div style='font-family:monospace; font-size:0.9em; line-height:1.5;'>"]
257
+ for ds_name, rels in sorted(tree.items()):
258
+ ds_label = rels.get("_dataset_label")
259
+ ds_badge = f" <span style='color:gray;'>[{html.escape(ds_label)}]</span>" if ds_label else ""
260
+ html_out.append(
261
+ f"<details open><summary><b style='color:#4da6ff;'>{html.escape(ds_name)}</b>{ds_badge}</summary><ul>"
262
+ )
263
+ for rel_type, targets in sorted(rels.items()):
264
+ if rel_type == "_dataset_label":
265
+ continue
266
+ for t in sorted(targets):
267
+ # βœ… Conditional rendering for usage context
268
+ if rel_type == "usage context":
269
+ html_out.append(
270
+ f"<li><span style='color:#80c904;'>{html.escape(rel_type)}</span>: "
271
+ f"<i>{html.escape(t)}</i></li>"
272
+ )
273
+ else:
274
+ html_out.append(
275
+ f"<li><span style='color:#80c904;'>{html.escape(rel_type)}</span>: {html.escape(t)}</li>"
276
+ )
277
+ html_out.append("</ul></details>")
278
+ html_out.append("</div>")
279
+
280
+ return "\n".join(html_out)
281
+
282
+ import html
283
+
284
+ def make_relation_tree_from_results(highlight_output, results):
285
+ """
286
+ Build relation tree directly from model results,
287
+ ensuring all relations (even those not highlighted) are captured.
288
+ """
289
+ if not results:
290
+ return "<i>No extracted relations found.</i>"
291
+
292
+ # Collect dataset mentions and their relations
293
+ datasets = []
294
+ tree = {}
295
 
296
+ for item in results:
297
+ ds = item.get("datasets")
298
+ rels = item.get("relations", [])
299
+ if not ds:
300
+ continue
301
 
302
+ ds_name = ds.get("text", "").strip()
303
+ ds_label = ds.get("label", "")
304
+ if not ds_name:
305
+ continue
306
 
307
+ # Initialize dataset node
308
+ if ds_name not in tree:
309
+ tree[ds_name] = {"_dataset_label": ds_label}
310
 
311
+ # Collect all relations
312
+ for rel in rels:
313
+ rel_type = rel.get("relation", "").strip().lower()
314
+ target = rel.get("target", "").strip()
315
 
316
+ # Skip empty or malformed relations
317
+ if not rel_type or not target:
318
+ continue
319
 
320
+ # Special handling for usage context (value-only)
321
+ if rel_type == "usage context" and "value" in rel:
322
+ target = rel["value"]
323
 
324
+ # Attach relation under this dataset
325
+ tree[ds_name].setdefault(rel_type, set()).add(target)
 
 
 
326
 
327
+ # Render HTML
328
+ html_out = ["<div style='font-family:monospace; font-size:0.9em; line-height:1.5;'>"]
329
+ for ds_name, rels in sorted(tree.items()):
330
+ ds_label = rels.get("_dataset_label", "")
331
+ ds_badge = f" <span style='color:gray;'>[{html.escape(ds_label)}]</span>" if ds_label else ""
332
+ html_out.append(
333
+ f"<details open><summary><b style='color:#4da6ff;'>{html.escape(ds_name)}</b>{ds_badge}</summary><ul>"
334
+ )
335
+ for rel_type, targets in sorted(rels.items()):
336
+ if rel_type == "_dataset_label":
 
 
 
 
 
 
 
 
 
 
 
337
  continue
338
+ for t in sorted(targets):
339
+ html_out.append(
340
+ f"<li><span style='color:#80c904;'>{html.escape(rel_type)}</span>: {html.escape(t)}</li>"
341
+ )
342
+ html_out.append("</ul></details>")
343
+ html_out.append("</div>")
344
+
345
+ return "\n".join(html_out)
346
+
347
+ import html
348
+ from collections import defaultdict
349
+ # ========================================
350
+ # Build Tree from Deduplicated Mentions
351
+ # ========================================
352
+ import html
353
+ import re
354
+
355
+ def make_relation_tree_from_dedup(dedup_results):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  """
357
+ Build a cleaned, deduplicated relation tree with prefiltering.
358
+ - Publishers that are dataset names are removed
359
+ - Years validated with regex \b\d{4}\b
360
+ - Empty/noisy values removed
361
+ """
362
+
363
+ if not dedup_results or not isinstance(dedup_results, list):
364
+ return "<i>No deduplicated results found.</i>"
365
+
366
+ # --- prepare dataset name list for filtering publisher noise ---
367
+ dataset_names = {d.get("text", "").lower() for d in dedup_results if d.get("text")}
368
+ relation_priority = [
369
+ "acronym", "author", "publisher", "data type", "data description",
370
+ "reference year", "publication year", "data geography",
371
+ "reference population", "usage context"
372
+ ]
373
+
374
+ def _filter_values(rel_type, values):
375
+ """relation-specific filtering logic"""
376
+ if not values:
377
+ return []
378
+
379
+ # ensure list
380
+ if isinstance(values, str):
381
+ values = [values]
382
+ elif not isinstance(values, (list, set, tuple)):
383
+ return []
384
+
385
+ clean = []
386
+ for v in values:
387
+ if not v or not isinstance(v, str):
388
+ continue
389
+ val = v.strip()
390
+ if not val:
391
+ continue
392
+
393
+ # publisher should not be dataset name
394
+ if rel_type == "publisher":
395
+ if val.lower() in dataset_names:
396
+ continue
397
+ # also skip if it's a dataset acronym
398
+ if any(val.lower() in str(a).lower() for d in dedup_results for a in d.get("acronym", [])):
399
+ continue
400
+
401
+ # year fields must contain 4-digit year
402
+ if rel_type in {"reference year", "publication year"}:
403
+ if not re.search(r"\b\d{4}\b", val):
404
+ continue
405
+
406
+ # simple length sanity check
407
+ if len(val) > 150:
408
+ continue
409
 
410
+ clean.append(val)
411
+ return sorted(set(clean))
412
 
413
+ # --- Build HTML output ---
414
+ html_out = ["<div style='font-family:monospace; font-size:0.9em; line-height:1.5;'>"]
415
+
416
+ for entry in dedup_results:
417
+ ds_name = entry.get("text", "").strip()
418
+ ds_label = entry.get("label", "")
419
+ if not ds_name:
420
+ continue
421
+
422
+ ds_badge = f" <span style='color:gray;'>[{html.escape(ds_label)}]</span>" if ds_label else ""
423
+ html_out.append(
424
+ f"<details open><summary><b style='color:#4da6ff;'>{html.escape(ds_name)}</b>{ds_badge}</summary><ul>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
+ rel_keys = [k for k in entry.keys() if k not in {
428
+ "text", "label", "score", "count", "form_counts", "other_datasets",
429
+ "mentioned_in_list", "mentioned_in_sentence_list", "pages",
430
+ "sources", "start_indices", "end_indices", "raw_contexts"
431
+ }]
432
 
433
+ rel_keys = sorted(
434
+ rel_keys,
435
+ key=lambda r: relation_priority.index(r) if r in relation_priority else len(relation_priority)
436
+ )
437
 
438
+ for rel_key in rel_keys:
439
+ vals = _filter_values(rel_key, entry.get(rel_key))
440
+ if not vals:
441
+ continue
442
+ html_out.append(
443
+ f"<li><span style='color:#80c904;'>{html.escape(rel_key)}</span>: "
444
+ f"{html.escape('; '.join(vals))}</li>"
445
  )
446
 
447
+ html_out.append("</ul></details>")
448
 
449
+ html_out.append("</div>")
450
+ return "\n".join(html_out)
451
+
452
+
453
+ with gr.Blocks() as demo:
454
+ gr.Markdown("## 🧠 AI for Data Use: Dataset Extraction (Deduplicated Tree)")
455
+
456
+ inp = gr.Textbox(
457
+ label="Input Text",
458
+ lines=10,
459
+ placeholder="Paste or type a paragraph here..."
460
  )
461
 
462
+ run_btn = gr.Button("πŸš€ Run Extraction")
463
+ out = gr.HighlightedText(label="Highlighted Datasets and Relations")
464
+
465
+ tree_btn = gr.Button("🧭 Show / Refresh Relation Tree")
466
+ tree_html = gr.HTML(label="Relation Tree", visible=False)
467
+
468
+ model_state = gr.State() # raw extract_from_text() results
469
+ highlight_state = gr.State() # processed highlights
470
+
471
  gr.Examples(
472
+ examples=example_texts,
473
+ inputs=inp,
474
+ label="πŸ§ͺ Try these examples"
 
 
 
 
 
475
  )
476
 
477
+ # --- main pipeline ---
478
+ def run_pipeline(text):
479
+ text = truncate_text(text, max_tokens=300)
480
+ highlights, results = process_text(text)
481
+ return highlights, results, highlights
482
+
483
+ run_btn.click(
484
+ fn=run_pipeline,
485
+ inputs=inp,
486
+ outputs=[out, model_state, highlight_state]
487
+ ).then(
488
+ fn=lambda h, m: gr.update(
489
+ visible=True,
490
+ value=make_relation_tree_from_dedup(deduplicate_mentions(m))
491
+ ),
492
+ inputs=[highlight_state, model_state],
493
+ outputs=tree_html
494
+ )
495
+
496
+ # --- manual refresh ---
497
+ def refresh_relation_tree(h, m):
498
+ if not h or not m:
499
+ return gr.update(visible=True, value="<i>No extracted data yet β€” run extraction first.</i>")
500
+ html_tree = make_relation_tree_from_dedup(deduplicate_mentions(m))
501
+ return gr.update(visible=True, value=html_tree)
502
+
503
+ tree_btn.click(fn=refresh_relation_tree, inputs=[highlight_state, model_state], outputs=tree_html)
504
+
505
+ gr.Markdown(f"""
506
  <hr style='border: none; border-top: 1px solid #333; margin: 20px 0;'>
507
 
508
  ### AI for Data Use: Dataset Extraction
 
537
  - Project Docs: https://worldbank.github.io/ai4data-use/docs/introduction.html
538
  """)
539
 
 
 
540
 
541
+ demo.launch()
requirements.txt CHANGED
@@ -1,7 +1 @@
1
- gradio>=4.0.0
2
- datasets
3
- transformers
4
- accelerate
5
- sentencepiece
6
- gliner
7
- huggingface_hub
 
1
+ gradio>=4.0.0