Spaces:
Sleeping
Sleeping
File size: 56,924 Bytes
86c1931 0336ee5 86c1931 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 | """
EGM Research Assistant β RAG + Visualisations
Gradio 5.x | sentence-transformers β ChromaDB β Claude + Plotly via HTML
"""
import os, re, io, base64
import pandas as pd
import chromadb
from chromadb.utils import embedding_functions
from anthropic import Anthropic
import gradio as gr
from collections import Counter
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import numpy as np
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
COLLECTION = "egm_studies"
MAX_TOKENS = 2000
BATCH_SIZE = 50
REGION_COLOURS = {
"Sub-Saharan Africa": "#2ecc71",
"Latin America and Caribbean": "#3498db",
"East Asia and Pacific": "#e74c3c",
"South Asia": "#f39c12",
"Middle East and North Africa": "#9b59b6",
"Europe and Central Asia": "#1abc9c",
"Multi-continent": "#95a5a6",
}
# ---------------------------------------------------------------------------
# Load dataset
# ---------------------------------------------------------------------------
print("Loading dataset...")
df = pd.read_csv("dataset.csv").fillna("")
# Normalise column name for PSE strategy (long name)
PSE_COL = "pse_strategy_multi_select_separate_values_with_single_vertical_bar"
CONF_COL = "overall_how_much_confidence_do_you_have_in_the_methods_used_to_analyse_the_findings_relative_to_the_primary_question_addressed_in_the_review"
df["year_of_publication"] = (
df["year_of_publication"].astype(str).str.replace(r"\.0$", "", regex=True)
)
RECORDS = df.to_dict("records")
TOTAL = len(RECORDS)
print(f"Loaded {TOTAL} studies.")
# ---------------------------------------------------------------------------
# Build embedding text β rich multi-field document
# ---------------------------------------------------------------------------
def make_doc(r):
parts = [
r.get("title", ""),
"Authors: " + r.get("authors", ""),
"Intervention: " + r.get("intervention", ""),
"Intervention detail: " + r.get("intervention_description", "")[:200],
"Intervention category: " + r.get("intervention_category", ""),
"Intervention group (PSE): "+ r.get("interv_PSE", ""),
"Outcome: " + r.get("outcome", ""),
"Outcome category: " + r.get("outcome_categories_assigned", ""),
"Outcome group (PSE): " + r.get("outc_PSE", ""),
"Theme: " + r.get("primary_theme", ""),
"Sub-theme: " + r.get("sub_primary_theme", ""),
"Sector: " + r.get("sector_name", ""),
"SDG: " + r.get("un_sustainable_development_goal", ""),
"Evaluation design: " + r.get("evaluation_design", ""),
"Evaluation method: " + r.get("evaluation_method", ""),
"PSE strategy: " + r.get(PSE_COL, ""),
"Country: " + r.get("country", ""),
"Region: " + r.get("region", ""),
"Income level: " + r.get("income_level", ""),
"GPI fragility rank: " + str(r.get("gpi_rank", "") or ""),
"Equity focus: " + r.get("equity_focus", ""),
"Population: " + r.get("population_description", "")[:150],
"Record type: " + r.get("record_type", ""),
"Method eval: " + r.get("method_eval", ""),
"Review confidence: " + r.get(CONF_COL, ""),
"Keywords: " + r.get("keywords", ""),
"Other topics: " + r.get("other_topics", ""),
r.get("abstract", "")[:600],
]
return " | ".join(p for p in parts if p.strip() and not p.strip().endswith(": "))
DOCS = [make_doc(r) for r in RECORDS]
IDS = [str(i) for i in range(TOTAL)]
METAS = [
{
"title": r["title"][:200],
"authors": r["authors"][:100],
"year": r["year_of_publication"],
"country": r["country"][:80],
"url": r.get("url_link", "")[:200] if str(r.get("url_link","")).startswith("http") else "",
}
for i, r in enumerate(RECORDS)
]
# ---------------------------------------------------------------------------
# ChromaDB vector index
# ---------------------------------------------------------------------------
print("Building vector index...")
ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=EMBED_MODEL)
chroma = chromadb.Client()
try:
chroma.delete_collection(COLLECTION)
except Exception:
pass
col = chroma.create_collection(
name=COLLECTION,
embedding_function=ef,
metadata={"hnsw:space": "cosine"},
)
for start in range(0, TOTAL, BATCH_SIZE):
end = min(start + BATCH_SIZE, TOTAL)
col.add(ids=IDS[start:end], documents=DOCS[start:end], metadatas=METAS[start:end])
print(f" Indexed {end}/{TOTAL} studies")
print("Vector index ready β")
# ---------------------------------------------------------------------------
# Anthropic client + system prompt
# ---------------------------------------------------------------------------
anthropic_client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY", ""))
SYSTEM = f"""You are a research synthesis assistant for an Evidence Gap Map (EGM) \
containing {TOTAL} studies on private sector engagement and development interventions.
STRICT RULES:
1. Answer ONLY using the studies explicitly provided in each message. Never use outside knowledge.
2. Only state what is explicitly present in the study data β do not generalise or assume.
3. Respect the diversity of study designs: studies may be Experimental, Quasi-experimental,
or Qualitative. Never describe the whole dataset as quantitative or experimental.
4. Every factual claim must have an inline [N] citation to a specific provided study.
5. If evidence is insufficient, say so and describe what the retrieved studies do cover.
RESPONSE FORMAT:
**Overview** β 2β3 sentences summarising what the evidence shows, noting study design diversity.
**Key Findings** β bullets grouped by theme/intervention, each with [N] citations.
**Study Designs & Methods** β note the methodological mix (RCTs, DiD, qualitative, etc.)
**Geographic & Context Coverage** β regions, income levels, GPI fragility rank (lower = more fragile), fragile/conflict-affected states if relevant.
**Equity & Population Focus** β note gender, equity dimensions, and target populations if relevant.
**Gaps & Caveats** β what is not well evidenced in the retrieved studies.
**Key Takeaways** β 2β3 concise conclusions from the provided studies only.
Use **bold** for key terms. Use [N] inline for every claim."""
# ---------------------------------------------------------------------------
# Retrieval + context builder
# ---------------------------------------------------------------------------
def retrieve(query, top_k):
top_k = min(int(top_k), TOTAL)
results = col.query(query_texts=[query], n_results=top_k,
include=["metadatas", "distances"])
studies = []
for i, doc_id in enumerate(results["ids"][0]):
rec = RECORDS[int(doc_id)].copy()
rec["_rank"] = i + 1
studies.append(rec)
return studies
def build_context(studies):
"""
Two-tier context:
- All studies: structured fields including PSE groups, keywords, GPI rank
- <=80 studies: also include abstract
"""
include_abstract = len(studies) <= 80
parts = []
for i, s in enumerate(studies, 1):
line = (
f"[{i}] \"{s['title']}\" | "
f"{s['authors']} ({s['year_of_publication']}) | "
f"{s['country']} | {s['region']} | Income: {s['income_level']} | "
f"GPI rank: {s.get('gpi_rank','')} | "
f"{s['record_type']} | {s['method_eval']} | "
f"{s['evaluation_design']} | {s['evaluation_method']} | "
f"Intervention: {s['intervention']} [{s['intervention_category']}] | "
f"PSE intervention group: {s.get('interv_PSE','')} | "
f"Outcome: {s['outcome']} [{s['outcome_categories_assigned']}] | "
f"PSE outcome group: {s.get('outc_PSE','')} | "
f"Theme: {s['primary_theme']} > {s['sub_primary_theme']} | "
f"Sector: {s['sector_name']} | Equity: {s['equity_focus']} | "
f"Keywords: {s.get('keywords','')} | Other topics: {s.get('other_topics','')} | "
f"Confidence: {s[CONF_COL]}"
)
if include_abstract and s.get("abstract","").strip():
line += f"\n Abstract: {s['abstract'][:250]}"
parts.append(line)
return "\n".join(parts)
# ---------------------------------------------------------------------------
# Charts rendered as HTML (avoids gr.Plot version conflicts)
# ---------------------------------------------------------------------------
CHART_BG = "#f8f9fa"
def detect_chart_topics(query):
"""Return exactly 1-2 most relevant chart types for the question."""
q = query.lower()
# --- SR appraisal check FIRST β if any SR keyword present, always show sr_appraisal ---
sr_triggers = [
"systematic review", "critical appraisal", "appraisal", "review quality",
"reliability of review", "reliability of the review", "srr", "amstar", "prisma",
"cochrane", "meta-analysis", "narrative synthesis", "review methodology",
"confidence in the methods", "rate the reliability",
]
if any(w in q for w in sr_triggers):
return {"sr_appraisal"}
# --- Non-SR scoring ---
scores = {
"interventions": 0,
"outcomes": 0,
"geo": 0,
"methods": 0,
"themes": 0,
"timeline": 0,
}
# Remove "systematic review" from method_words β handled above
geo_words = ["country","countries","region","africa","asia","latin","europe","pacific",
"middle east","where","location","fragil","conflict","gpi","income",
"low income","middle income","global","south asia","sub-saharan"]
method_words = ["method","design","rct","randomis","experiment","qualitative","quasi",
"evaluation","evidence type","how studied","quantitative","approach",
"study type","study design"]
theme_words = ["theme","sector","health","education","finance","economic","social",
"environment","energy","urban","rural","topic","area"]
interv_words = ["intervention","program","programme","activity","what works","pse","ppp",
"cash transfer","voucher","training","tvet","microfinance","credit",
"matchmaking","subsidy","capacity","financing"]
outcome_words = ["outcome","impact","effect","result","employment","income","job","market",
"growth","food","nutrition","wellbeing","welfare","earnings"]
time_words = ["year","recent","latest","trend","over time","timeline",
"2020","2021","2022","2023","2024","when","publish"]
for w in geo_words: scores["geo"] += q.count(w)
for w in method_words: scores["methods"] += q.count(w)
for w in theme_words: scores["themes"] += q.count(w)
for w in interv_words: scores["interventions"] += q.count(w)
for w in outcome_words: scores["outcomes"] += q.count(w)
for w in time_words: scores["timeline"] += q.count(w)
ranked = sorted(scores.items(), key=lambda x: -x[1])
top = [k for k, v in ranked if v > 0][:2]
if not top:
top = ["interventions", "geo"]
elif len(top) == 1:
pairs = {
"interventions": "outcomes",
"outcomes": "interventions",
"geo": "interventions",
"methods": "interventions",
"themes": "interventions",
"timeline": "interventions",
}
top.append(pairs[top[0]])
return set(top[:2])
def make_single_chart(kind, studies):
"""Make one matplotlib chart and return as HTML img tag."""
NAVY = "#0a2342"
BLUE = "#3498db"
RED = "#e74c3c"
AMBER = "#f39c12"
PURPLE = "#9b59b6"
GREEN = "#2ecc71"
GREY = "#95a5a6"
BG = "#f8f9fa"
fig, ax = plt.subplots(figsize=(9, 4), facecolor=BG)
ax.set_facecolor(BG)
for spine in ax.spines.values():
spine.set_color("#e2e8f0")
if kind == "interventions":
ivs = []
for s in studies:
for iv in s.get("interv_PSE","").split("|"):
iv = iv.strip()
if iv and iv.lower() not in ("","not applicable","nan"):
ivs.append(iv)
if not ivs:
for s in studies:
iv = s.get("intervention_category","").strip()
if iv and iv.lower() not in ("","not applicable","nan"):
ivs.append(iv)
counts = Counter(ivs).most_common(10)
if not counts: return None
labels = [x[0][:40] for x in counts][::-1]
values = [x[1] for x in counts][::-1]
bars = ax.barh(labels, values, color=BLUE, edgecolor="white", height=0.6)
for bar, val in zip(bars, values):
ax.text(bar.get_width()+0.1, bar.get_y()+bar.get_height()/2,
str(val), va="center", ha="left", fontsize=8, color="#444")
ax.set_xlabel("Studies", fontsize=9, color="#666")
ax.set_xlim(0, max(values)*1.15)
ax.set_title("PSE Intervention Groups in retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "outcomes":
ocs = []
for s in studies:
for oc in s.get("outc_PSE","").split("|"):
oc = oc.strip()
if oc and oc.lower() not in ("","not applicable","nan"):
ocs.append(oc)
if not ocs:
for s in studies:
for oc in s.get("outcome_categories_assigned","").split("|"):
oc = oc.strip()
if oc and oc.lower() not in ("","not applicable","nan"):
ocs.append(oc)
counts = Counter(ocs).most_common(10)
if not counts: return None
labels = [x[0][:40] for x in counts][::-1]
values = [x[1] for x in counts][::-1]
colors = [GREEN if i % 2 == 0 else "#1abc9c" for i in range(len(labels))]
bars = ax.barh(labels, values, color=colors, edgecolor="white", height=0.6)
for bar, val in zip(bars, values):
ax.text(bar.get_width()+0.1, bar.get_y()+bar.get_height()/2,
str(val), va="center", ha="left", fontsize=8, color="#444")
ax.set_xlabel("Studies", fontsize=9, color="#666")
ax.set_xlim(0, max(values)*1.15)
ax.set_title("PSE Outcome Groups in retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "geo":
regions = []
for s in studies:
r = s.get("region","").split("|")[0].strip()
if r and r.lower() not in ("","not applicable","nan"):
regions.append(r)
counts = Counter(regions)
if not counts: return None
rl = list(counts.keys())
rv = list(counts.values())
rcols = [REGION_COLOURS.get(r, GREY) for r in rl]
wedges, _, autotexts = ax.pie(
rv, labels=None, colors=rcols, autopct="%1.0f%%",
startangle=90, wedgeprops=dict(width=0.55, edgecolor="white"),
pctdistance=0.75,
)
for t in autotexts:
t.set_fontsize(9); t.set_color("white")
ax.legend(wedges, [r[:28] for r in rl], loc="center left",
bbox_to_anchor=(1.0, 0.5), fontsize=8, frameon=False)
ax.set_title("Regional distribution of retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "methods":
designs = [s.get("evaluation_design","").strip() for s in studies
if s.get("evaluation_design","").strip() not in ("","Not applicable","nan")]
counts = Counter(designs)
if not counts: return None
dl = list(counts.keys()); dv = list(counts.values())
dc = {"Experimental":RED, "Quasi-experimental":BLUE, "Qualitative":GREEN}
dcols = [dc.get(d, GREY) for d in dl]
wedges2, _, autotexts2 = ax.pie(
dv, labels=None, colors=dcols, autopct="%1.0f%%",
startangle=90, wedgeprops=dict(width=0.55, edgecolor="white"),
pctdistance=0.75,
)
for t in autotexts2:
t.set_fontsize(9); t.set_color("white")
ax.legend(wedges2, dl, loc="center left",
bbox_to_anchor=(1.0, 0.5), fontsize=8, frameon=False)
ax.set_title("Study designs in retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "themes":
themes = []
for s in studies:
t = s.get("primary_theme","").strip()
if t and t.lower() not in ("","not applicable","nan"):
themes.append(t)
counts = Counter(themes).most_common(8)
if not counts: return None
labels = [x[0][:35] for x in counts][::-1]
values = [x[1] for x in counts][::-1]
palette = ["#0a2342","#1a3a5c","#1e5280","#3498db","#5dade2","#85c1e9","#aed6f1","#d6eaf8"]
colors = [palette[i % len(palette)] for i in range(len(labels))]
bars = ax.barh(labels, values, color=colors, edgecolor="white", height=0.6)
for bar, val in zip(bars, values):
ax.text(bar.get_width()+0.1, bar.get_y()+bar.get_height()/2,
str(val), va="center", ha="left", fontsize=8, color="#444")
ax.set_xlabel("Studies", fontsize=9, color="#666")
ax.set_xlim(0, max(values)*1.15)
ax.set_title("Themes in retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "timeline":
years = []
for s in studies:
try: years.append(int(float(s.get("year_of_publication",""))))
except: pass
if not years: return None
yc = sorted(Counter(years).items())
ax.plot([x[0] for x in yc], [x[1] for x in yc],
color=RED, linewidth=2.5, marker="o", markersize=5)
ax.fill_between([x[0] for x in yc], [x[1] for x in yc], alpha=0.12, color=RED)
ax.set_xlabel("Year", fontsize=9, color="#666")
ax.set_ylabel("Studies", fontsize=9, color="#666")
ax.set_title("Publication years of retrieved studies", fontsize=11, fontweight="bold", color=NAVY, pad=8)
elif kind == "sr_appraisal":
plt.close(fig)
_N="#0a2342"; _G="#2ecc71"; _A="#f39c12"; _R="#e74c3c"; _BG2="#f8f9fa"
# Always use ALL SR studies from the full dataset for the appraisal chart
sr_studies = [r for r in RECORDS if r.get("record_type","") == "srr"]
if not sr_studies:
fig2, ax2 = plt.subplots(figsize=(8, 3), facecolor=_BG2)
ax2.text(0.5, 0.5, "No systematic reviews found in dataset",
ha="center", va="center", transform=ax2.transAxes, color="#aaa", fontsize=11)
ax2.axis("off")
return fig_to_html(fig2)
n_sr = len(sr_studies)
# All appraisal criteria
criteria = {
"Reliability of review": "sr_reliability",
"Confidence: analysis methods": "sr_confidence_analysis",
"Confidence: inclusion methods": "sr_confidence_inclusion",
"Quality/bias criteria used": "sr_quality_criteria",
"Inclusion criteria reported": "sr_inclusion_criteria",
"Comprehensive search": "sr_comprehensive_search",
"Selection bias avoided": "sr_selection_bias",
"Independent screening": "sr_independent_screening",
"Independent RoB assessment": "sr_independent_rob",
"Independent data extraction": "sr_data_extraction",
"Language bias avoided": "sr_language_bias",
"Grey literature included": "sr_grey_literature",
"Relevant databases searched": "sr_databases_searched",
}
YES_VALS = {"yes","high","low risk","adequate"}
PARTIAL_VALS = {"partially","partial","moderate","unclear","can't tell"}
NO_VALS = {"no","low","high risk","inadequate","not applicable"}
labels_list, yes_c, partial_c, no_c, missing_c = [], [], [], [], []
for label, col in criteria.items():
vals = [str(s.get(col,"")).strip().lower() for s in sr_studies]
y = sum(1 for v in vals if v in YES_VALS)
p = sum(1 for v in vals if v in PARTIAL_VALS)
n = sum(1 for v in vals if v in NO_VALS)
m = n_sr - y - p - n
labels_list.append(label)
yes_c.append(y); partial_c.append(p)
no_c.append(n); missing_c.append(m)
fig2, ax2 = plt.subplots(figsize=(11, 6), facecolor=_BG2)
ax2.set_facecolor(_BG2)
y_pos = list(range(len(labels_list)))
ax2.barh(y_pos, yes_c, color=_G, edgecolor="white", label="Yes / High", height=0.6)
ax2.barh(y_pos, partial_c, color=_A, edgecolor="white", label="Partial", height=0.6,
left=yes_c)
ax2.barh(y_pos, no_c, color=_R, edgecolor="white", label="No / Low", height=0.6,
left=[y+p for y,p in zip(yes_c, partial_c)])
ax2.barh(y_pos, missing_c, color="#d5d8dc", edgecolor="white", label="Not recorded", height=0.6,
left=[y+p+n for y,p,n in zip(yes_c, partial_c, no_c)])
ax2.set_yticks(y_pos)
ax2.set_yticklabels(labels_list, fontsize=9, color="#333")
ax2.set_xlabel(f"Number of systematic reviews (n={n_sr})", fontsize=9, color="#666")
ax2.set_xlim(0, n_sr + 0.5)
ax2.set_xticks(range(0, n_sr + 1))
ax2.set_title(f"Critical Appraisal of {n_sr} Systematic Reviews",
fontsize=12, fontweight="bold", color=_N, pad=10)
ax2.legend(loc="lower right", fontsize=8, frameon=False, ncol=2)
for spine in ax2.spines.values(): spine.set_color("#e2e8f0")
ax2.tick_params(colors="#666")
plt.tight_layout()
return fig_to_html(fig2)
ax.tick_params(labelsize=8, colors="#444")
plt.tight_layout()
return fig_to_html(fig)
def fig_to_html(fig):
"""Convert matplotlib figure to base64 PNG embedded as HTML img tag."""
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=130, bbox_inches="tight",
facecolor=fig.get_facecolor())
plt.close(fig)
buf.seek(0)
b64 = base64.b64encode(buf.read()).decode()
return f'<img src="data:image/png;base64,{b64}" style="width:100%;border-radius:8px;margin-top:8px;">'
def make_chart_html(studies, query=""):
"""Generate exactly 1-2 charts most relevant to the query."""
topics = detect_chart_topics(query) # always returns exactly 2
html_parts = []
for topic in ["interventions","outcomes","geo","methods","themes","timeline","sr_appraisal"]:
if topic in topics:
try:
chart = make_single_chart(topic, studies)
if chart:
html_parts.append(chart)
except Exception as e:
html_parts.append(
f"<p style='color:#e74c3c;font-size:12px;'>Chart error ({topic}): {e}</p>"
)
if not html_parts:
return EMPTY_HTML
label_map = {
"interventions": "Interventions",
"outcomes": "Outcomes",
"geo": "Geography",
"methods": "Study designs",
"themes": "Themes",
"timeline": "Timeline",
"sr_appraisal": "SR Critical Appraisal",
}
topic_labels = " + ".join(label_map[t] for t in topics if t in label_map)
header = (
f'''<div style="font-family:Inter,sans-serif;font-size:12px;color:#888;
padding:4px 0 8px;">π <b style="color:#0a2342;">{topic_labels}</b>
β based on {len(studies)} retrieved studies</div>'''
)
return header + "".join(html_parts)
EMPTY_HTML = """<div style="height:180px;display:flex;align-items:center;justify-content:center;
background:#f8f9fa;border-radius:8px;color:#adb5bd;font-size:14px;font-family:Inter,sans-serif;">
Ask a question β charts will show the profile of studies retrieved for that specific question</div>"""
# ---------------------------------------------------------------------------
# Chat β Gradio 5.x messages format
# ---------------------------------------------------------------------------
def chat(user_message, history, top_k):
if not user_message.strip():
yield history, EMPTY_HTML
return
studies = retrieve(user_message, top_k)
context = build_context(studies)
augmented = (
f"RETRIEVED STUDIES ({len(studies)} of {TOTAL} total):\n\n{context}\n\n---\n"
f"Using ONLY the studies above, answer:\n{user_message}"
)
api_messages = []
for turn in history:
role = turn.get("role", "")
content = turn.get("content", "")
if isinstance(content, list):
content = " ".join(b.get("text","") if isinstance(b,dict) else str(b) for b in content)
if role in ("user","assistant") and content:
api_messages.append({"role": role, "content": content})
api_messages.append({"role": "user", "content": augmented})
history = history + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": ""},
]
partial = ""
try:
stream_ctx = anthropic_client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=MAX_TOKENS,
system=SYSTEM,
messages=api_messages,
)
with stream_ctx as stream:
for text in stream.text_stream:
partial += text
history[-1]["content"] = partial
yield history, EMPTY_HTML
except Exception as e:
err = str(e)
if "rate_limit" in err or "429" in err:
history[-1]["content"] = "β οΈ **Rate limit reached.** Too many studies were sent at once. Please reduce the **Studies retrieved** slider to 20β25 and try again."
else:
history[-1]["content"] = f"β οΈ **API error:** {err[:300]}"
yield history, EMPTY_HTML
return
# Append reference list
cited = sorted(set(int(n) for n in re.findall(r"\[(\d+)\]", partial)))
if cited:
refs = ["\n\n---\n**Studies cited:**"]
for n in cited:
if 1 <= n <= len(studies):
s = studies[n-1]
url = s.get("url_link","")
lnk = f"[{s['title']}]({url})" if url and url.startswith("http") else s["title"]
refs.append(
f"**[{n}]** {lnk} \n"
f"*{s['authors']} ({s['year_of_publication']}) Β· "
f"{s['country']} Β· {s['evaluation_design']}*"
)
history[-1]["content"] += "\n".join(refs)
try:
chart_html = make_chart_html(studies, query=user_message)
except Exception as e:
chart_html = f"<p style='color:#e74c3c;font-family:sans-serif'>Chart could not be generated: {e}</p>"
yield history, chart_html
def clear_chat():
return [], EMPTY_HTML
# ---------------------------------------------------------------------------
# Gradio 5.x UI
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Dataset Summary β direct column counts, no RAG/LLM needed
# ---------------------------------------------------------------------------
# Columns available for summary, with friendly labels
SUMMARY_COLS = {
"Intervention Category": "intervention_category",
"PSE Intervention Group": "interv_PSE",
"Outcome Category": "outcome_categories_assigned",
"PSE Outcome Group": "outc_PSE",
"Primary Theme": "primary_theme",
"Sub-theme": "sub_primary_theme",
"Sector": "sector_name",
"Evaluation Design": "evaluation_design",
"Method / Study Type": "method_eval",
"Region": "region",
"Country": "country",
"Income Level": "income_level",
"GPI Fragility Rank": "gpi_rank",
"Equity Focus": "equity_focus",
"Record Type": "record_type",
"Publication Type": "publication_type",
"SDG": "un_sustainable_development_goal",
"Funding Agency": "program_funding_agency",
"Implementation Agency": "implementation_agencies",
"Critical Appraisal of SRs": "sr_appraisal_summary",
}
SUMMARY_COLOURS = [
"#3498db","#2ecc71","#e74c3c","#f39c12","#9b59b6",
"#1abc9c","#e67e22","#34495e","#e91e63","#00bcd4",
"#8bc34a","#ff5722","#607d8b","#795548","#9c27b0",
]
def make_summary_chart(col_label):
col = SUMMARY_COLS.get(col_label)
if not col:
return "<p>Select a column to summarise.</p>"
# Virtual key β delegate to SR appraisal chart directly
if col == "sr_appraisal_summary":
para_white = "Critical appraisal ratings across all <b style=\"color:#ffffff;font-weight:700;\">13 systematic reviews</b> in the dataset, assessed across 13 quality criteria. Bars show <b style=\"color:#ffffff;font-weight:700;\">Yes/High</b> (green), <b style=\"color:#ffffff;font-weight:700;\">Partial</b> (amber), <b style=\"color:#ffffff;font-weight:700;\">No/Low</b> (red), and not recorded (grey). Most reviews are rated <b style=\"color:#ffffff;font-weight:700;\">Low</b> on reliability and confidence."
desc_html = f"""<div style="background:linear-gradient(135deg,#0a2342 0%,#1a3a5c 100%);border-radius:10px;padding:18px 22px;margin-bottom:14px;font-family:Inter,sans-serif;font-size:14px;line-height:1.8;box-shadow:0 4px 16px rgba(10,35,66,0.25);"><div style="font-size:12px;font-weight:700;color:rgba(255,255,255,0.65);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.8px;">Critical Appraisal of Systematic Reviews Β· 13 studies Β· 13 criteria</div><span style="color:#ffffff;display:block;">{para_white}</span></div>"""
chart = make_single_chart("sr_appraisal", [])
return desc_html + (chart or "<p>No SR data found.</p>")
series = df[col].replace("", pd.NA).dropna()
if series.empty:
return f"<p>No data available for <b>{col_label}</b>.</p>"
all_vals = []
for v in series:
for part in str(v).split("|"):
part = part.strip()
if part and part.lower() not in ("not applicable", "nan", "n/a", "multi-country"):
all_vals.append(part)
counts = Counter(all_vals).most_common(20)
if not counts:
return f"<p>No data available for <b>{col_label}</b>.</p>"
total = sum(v for _, v in counts)
n_cats = len(Counter(all_vals))
coverage = len(series)
missing = TOTAL - coverage
top1 = counts[0]
top2 = counts[1] if len(counts) > 1 else None
top3 = counts[2] if len(counts) > 2 else None
top3_sum = sum(v for _, v in counts[:3])
# Full paragraph description
cat_word = "category" if n_cats == 1 else "categories"
rem_word = "category" if n_cats - 3 == 1 else "categories"
para = (
f"Across {coverage} studies with available data, "
f"<b>{col_label}</b> spans <b>{n_cats} distinct {cat_word}</b>. "
f"The most represented is <b>{top1[0]}</b> with {top1[1]} studies ({top1[1]/total*100:.1f}% of all values)"
)
if top2:
para += f", followed by <b>{top2[0]}</b> ({top2[1]} studies, {top2[1]/total*100:.1f}%)"
if top3:
para += f" and <b>{top3[0]}</b> ({top3[1]} studies, {top3[1]/total*100:.1f}%)"
para += f". Together, the top 3 categories account for <b>{top3_sum/total*100:.0f}%</b> of all values"
if n_cats > 3:
para += f", while the remaining {n_cats - 3} {rem_word} collectively represent {(total - top3_sum)/total*100:.0f}%"
para += ". "
if missing > 0:
para += f"Note: {missing} of {TOTAL} studies have no value recorded for this field."
else:
para += f"Data is complete β all {TOTAL} studies have a value for this field."
# Replace <b> tags with white inline spans so browser defaults don't override
para_white = para.replace("<b>", '<b style="color:#ffffff;font-weight:700;">').replace("</b>", "</b>")
desc_html = f"""<div style="background:linear-gradient(135deg,#0a2342 0%,#1a3a5c 100%);
border-radius:10px;padding:18px 22px;margin-bottom:14px;
font-family:Inter,sans-serif;font-size:14px;line-height:1.8;
box-shadow:0 4px 16px rgba(10,35,66,0.25);">
<div style="font-size:12px;font-weight:700;color:rgba(255,255,255,0.65);
margin-bottom:8px;text-transform:uppercase;letter-spacing:0.8px;">
{col_label} Β· {n_cats} categories Β· {coverage} studies
</div>
<span style="color:#ffffff;display:block;">{para_white}</span>
</div>"""
# GPI Rank β histogram
if col == "gpi_rank":
gpi_vals = []
for v in series:
try:
fv = float(str(v).replace(",",""))
if fv > 0:
gpi_vals.append(fv)
except: pass
if not gpi_vals:
return desc_html + "<p style='color:#888;'>No GPI rank data available.</p>"
fig2, ax = plt.subplots(figsize=(10, 4), facecolor="#f8f9fa")
ax.set_facecolor("#f8f9fa")
ax.hist(gpi_vals, bins=20, color="#9b59b6", edgecolor="white", linewidth=0.5)
ax.set_xlabel("GPI Rank (β more fragile | less fragile β)", fontsize=10, color="#666")
ax.set_ylabel("Number of studies", fontsize=10, color="#666")
ax.tick_params(labelsize=9, colors="#444")
for spine in ax.spines.values(): spine.set_color("#e2e8f0")
plt.tight_layout()
return desc_html + fig_to_html(fig2)
# World map for Country
if col == "country":
try:
import geopandas as gpd
except ImportError:
# Fallback: bar chart of top countries
country_counts = Counter(all_vals)
top_c = country_counts.most_common(25)
fig2, ax = plt.subplots(figsize=(10, 8), facecolor="#f8f9fa")
ax.set_facecolor("#f8f9fa")
c_labels = [x[0][:25] for x in top_c][::-1]
c_values = [x[1] for x in top_c][::-1]
c_colors = ["#0a2342" if v == max(c_values) else
"#1a5276" if v >= max(c_values)*0.5 else
"#3498db" if v >= max(c_values)*0.2 else
"#85c1e9" for v in c_values]
bars = ax.barh(c_labels, c_values, color=c_colors, edgecolor="white", height=0.7)
for bar, val in zip(bars, c_values):
ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2,
str(val), va="center", ha="left", fontsize=8, color="#444")
ax.set_xlabel("Studies", fontsize=10, color="#666")
ax.set_title("Top 25 Countries by Study Count", fontsize=11,
fontweight="bold", color="#0a2342", pad=8)
ax.tick_params(labelsize=8, colors="#444")
for spine in ax.spines.values(): spine.set_color("#e2e8f0")
plt.tight_layout()
return desc_html + fig_to_html(fig2)
# Bar chart for all other columns
bar_labels = [x[0][:45] for x in counts][::-1]
bar_values = [x[1] for x in counts][::-1]
bar_pcts = [f"{v/total*100:.1f}%" for v in bar_values]
bar_colors = [SUMMARY_COLOURS[i % len(SUMMARY_COLOURS)] for i in range(len(bar_labels))]
fig2, ax = plt.subplots(figsize=(10, max(4, len(bar_labels) * 0.5 + 1)),
facecolor="#f8f9fa")
ax.set_facecolor("#f8f9fa")
bars = ax.barh(bar_labels, bar_values, color=bar_colors, edgecolor="white", height=0.65)
for bar, val, pct in zip(bars, bar_values, bar_pcts):
ax.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2,
f"{val} ({pct})", va="center", ha="left", fontsize=8, color="#444")
ax.set_xlabel("Number of studies", fontsize=10, color="#666")
ax.set_xlim(0, max(bar_values) * 1.25)
ax.tick_params(labelsize=9, colors="#444")
for spine in ax.spines.values(): spine.set_color("#e2e8f0")
plt.tight_layout()
return desc_html + fig_to_html(fig2)
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Evidence Gap Analysis
# ---------------------------------------------------------------------------
INTERVENTIONS = [
"Capacity development of private, public, or hybrid sector actors",
"Public-private partnerships (PPPs)",
"Multicomponent",
"Financing with private sector actors",
"Financing of private sector actors",
"Matchmaking and consulting",
"Structured dialogue",
]
OUTCOMES = ["Work-Labor/Social","Jobs and Income","Market/Sector Growth",
"Coordination/Reforms","Environmental","Energy/Extractives"]
SDGS = [
"Decent Work and Economic Growth","Good Health and Well-being","Quality Education",
"No Poverty","Industry, Innovation and Infrastructure","Reduced Inequality",
"Gender Equality","Zero Hunger","Climate Action",
]
INTERV_PSE = [
"Technical and vocational training",
"Capacity strengthening to enhance private sector engagement",
"Service management and delivery PPP",
"Direct financing of private sector actors",
"Payments by results",
"Matchmaking",
"Infrastructure development and financing",
"Advisory support",
"Multi-stakeholder platforms",
"Blended finance",
]
OUTC_PSE = [
"Employment","Income","Food security, health, and nutrition","Education",
"Domestic business growth and market development",
"Training, knowledge, and technology transfer",
"Social factors","Productivity gains","Financial inclusion","International trade",
]
INT_SHORT = {
"Capacity development of private, public, or hybrid sector actors": "Capacity Dev.",
"Public-private partnerships (PPPs)": "PPPs",
"Multicomponent": "Multicomponent",
"Financing with private sector actors": "Financing (with)",
"Financing of private sector actors": "Financing (of)",
"Matchmaking and consulting": "Matchmaking",
"Structured dialogue": "Structured Dialogue",
}
SDG_SHORT = {
"Decent Work and Economic Growth": "SDG8: Decent Work",
"Good Health and Well-being": "SDG3: Health",
"Quality Education": "SDG4: Education",
"No Poverty": "SDG1: No Poverty",
"Industry, Innovation and Infrastructure": "SDG9: Industry",
"Reduced Inequality": "SDG10: Inequality",
"Gender Equality": "SDG5: Gender",
"Zero Hunger": "SDG2: Zero Hunger",
"Climate Action": "SDG13: Climate",
}
def build_matrix(row_keys, col_keys, row_col, col_col, split_col=False, dff=None):
if dff is None:
dff = df
mat = pd.DataFrame(0, index=row_keys, columns=col_keys)
for _, row in dff.iterrows():
rval = str(row.get(row_col, "")).strip()
if not rval or rval not in row_keys:
# also try splitting row value
for rp in rval.split("|"):
rp = rp.strip()
if rp in row_keys:
cvals = str(row.get(col_col, "")).split("|") if split_col else [str(row.get(col_col,""))]
for cv in cvals:
cv = cv.strip()
if cv in col_keys:
mat.loc[rp, cv] += 1
continue
cvals = str(row.get(col_col, "")).split("|") if split_col else [str(row.get(col_col,""))]
for cv in cvals:
cv = cv.strip()
if cv in col_keys:
mat.loc[rval, cv] += 1
return mat
def make_gap_heatmap(matrix, row_labels, col_labels, title, height=420):
"""Render gap heatmap as PNG using matplotlib."""
z = matrix.values
rows = [row_labels.get(r, r) for r in matrix.index]
cols = [col_labels.get(c, c) for c in matrix.columns]
nrows, ncols = z.shape
fig, ax = plt.subplots(figsize=(max(10, ncols * 1.4), max(5, nrows * 0.8)),
facecolor="#f8f9fa")
ax.set_facecolor("#f8f9fa")
# Custom colormap: red (0) β amber β yellow β blue β dark navy
import matplotlib.colors as mcolors
cmap = mcolors.LinearSegmentedColormap.from_list("egm", [
"#e74c3c", "#f39c12", "#f1c40f", "#3498db", "#0a2342"
])
norm = mcolors.Normalize(vmin=0, vmax=max(z.max(), 1))
im = ax.imshow(z, cmap=cmap, norm=norm, aspect="auto")
# Annotations
max_val = z.max() or 1
for i in range(nrows):
for j in range(ncols):
val = z[i, j]
txt = "GAP" if val == 0 else str(int(val))
col = "white" if (val == 0 or val / max_val > 0.3) else "#2c3e50"
ax.text(j, i, txt, ha="center", va="center",
fontsize=9, color=col, fontweight="bold" if val == 0 else "normal")
ax.set_xticks(range(ncols))
ax.set_xticklabels([c[:30] for c in cols], rotation=40, ha="right", fontsize=8, color="#444")
ax.set_yticks(range(nrows))
ax.set_yticklabels(rows, fontsize=8, color="#444")
ax.set_title(title, fontsize=12, fontweight="bold", color="#0a2342", pad=10)
cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.02)
cbar.set_label("Studies", fontsize=9, color="#666")
cbar.ax.tick_params(labelsize=8)
for spine in ax.spines.values():
spine.set_visible(False)
plt.tight_layout()
return fig
def make_gap_summary(matrix, row_labels, col_labels, type_label):
total_cells = matrix.size
gap_cells = int((matrix == 0).sum().sum())
strong_cells = int((matrix >= 10).sum().sum())
limited_cells = int(((matrix >= 1) & (matrix < 5)).sum().sum())
flat = matrix.stack().reset_index()
flat.columns = ["row", "col", "count"]
flat = flat[flat["count"] > 0].sort_values("count", ascending=False)
top_combo = ""
if not flat.empty:
t = flat.iloc[0]
top_combo = (
f"The best-evidenced combination is "
f"<b>{row_labels.get(t['row'], t['row'])}</b> Γ "
f"<b>{col_labels.get(t['col'], t['col'])}</b> ({int(t['count'])} studies). "
)
gaps = [(row_labels.get(r,""), col_labels.get(c,""))
for r in matrix.index for c in matrix.columns if matrix.loc[r,c] == 0]
gap_examples = "; ".join(f"{r} Γ {c}" for r, c in gaps[:3])
parts = type_label.split(" Γ ")
dim2 = parts[1] if len(parts) > 1 else type_label
para = (
f"The matrix covers <b>{total_cells} possible combinations</b> "
f"({len(matrix.index)} rows Γ {len(matrix.columns)} {dim2} values). "
f"<b>{gap_cells} combinations ({gap_cells/total_cells*100:.0f}%) have no evidence</b> β "
f"clear gaps where research is needed. "
f"<b>{strong_cells} combinations ({strong_cells/total_cells*100:.0f}%)</b> have 10+ studies. "
f"{top_combo}"
+ (f"Key gaps include: {gap_examples}." if gaps else "")
)
badges = (
f'''<div style="display:flex;gap:10px;margin-top:12px;flex-wrap:wrap;">
<span style="background:#e74c3c;color:white;border-radius:6px;padding:5px 12px;font-size:12px;font-weight:600;">π΄ {gap_cells} Gaps</span>
<span style="background:#e67e22;color:white;border-radius:6px;padding:5px 12px;font-size:12px;font-weight:600;">π‘ {limited_cells} Limited (<5)</span>
<span style="background:#1a5276;color:white;border-radius:6px;padding:5px 12px;font-size:12px;font-weight:600;">π΅ {strong_cells} Strong (10+)</span>
</div>'''
)
para_white = para.replace("<b>", '<b style="color:#ffffff;font-weight:700;">').replace("</b>", "</b>")
return f'''<div style="background:linear-gradient(135deg,#0a2342 0%,#1a3a5c 100%);
border-radius:10px;padding:18px 22px;margin-bottom:14px;
font-family:Inter,sans-serif;font-size:14px;line-height:1.8;
box-shadow:0 4px 16px rgba(10,35,66,0.25);">
<div style="font-size:12px;font-weight:700;color:rgba(255,255,255,0.65);
margin-bottom:8px;text-transform:uppercase;letter-spacing:0.8px;">
Evidence Gap Analysis Β· {type_label}
</div>
<span style="color:#ffffff;display:block;">{para_white}</span>
{badges}
</div>'''
def make_gap_tab_content(analysis_type, method_filter="All"):
if method_filter == "IE Quantitative":
dff = df[df["method_eval"] == "IE Quantitative"]
elif method_filter == "IE Qualitative":
dff = df[df["method_eval"] == "IE Qualitative"]
elif method_filter == "SR (Systematic Review)":
dff = df[df["method_eval"] == "SR"]
else:
dff = df
n = len(dff)
sfx = f" [{method_filter}, {n} studies]" if method_filter != "All" else ""
configs = {
"Intervention Category Γ Outcome Category": dict(
rows=INTERVENTIONS, cols=OUTCOMES,
rc="intervention_category", cc="outcome_categories_assigned",
rl=INT_SHORT, cl={o: o for o in OUTCOMES}, h=400,
),
"Intervention Category Γ SDG": dict(
rows=INTERVENTIONS, cols=SDGS,
rc="intervention_category", cc="un_sustainable_development_goal",
rl=INT_SHORT, cl=SDG_SHORT, h=420,
),
"Outcome Category Γ SDG": dict(
rows=OUTCOMES, cols=SDGS,
rc="outcome_categories_assigned", cc="un_sustainable_development_goal",
rl={o: o for o in OUTCOMES}, cl=SDG_SHORT, h=380,
),
"PSE Intervention Γ PSE Outcome": dict(
rows=INTERV_PSE, cols=OUTC_PSE,
rc="interv_PSE", cc="outc_PSE",
rl={i: i for i in INTERV_PSE}, cl={o: o for o in OUTC_PSE}, h=480,
),
"PSE Intervention Γ SDG": dict(
rows=INTERV_PSE, cols=SDGS,
rc="interv_PSE", cc="un_sustainable_development_goal",
rl={i: i for i in INTERV_PSE}, cl=SDG_SHORT, h=480,
),
"PSE Outcome Γ SDG": dict(
rows=OUTC_PSE, cols=SDGS,
rc="outc_PSE", cc="un_sustainable_development_goal",
rl={o: o for o in OUTC_PSE}, cl=SDG_SHORT, h=460,
),
}
cfg = configs.get(analysis_type, configs["Intervention Category Γ Outcome Category"])
mat = build_matrix(cfg["rows"], cfg["cols"], cfg["rc"], cfg["cc"],
split_col=True, dff=dff)
summary_html = make_gap_summary(mat, cfg["rl"], cfg["cl"], analysis_type + sfx)
fig = make_gap_heatmap(mat, cfg["rl"], cfg["cl"],
f"Evidence Map: {analysis_type}{sfx}", height=cfg["h"])
try:
chart_html = fig_to_html(fig)
except Exception as e:
chart_html = f"<p style='color:red'>Chart error: {e}</p>"
return summary_html + chart_html
# ---------------------------------------------------------------------------
# Custom CSS β 3ie professional dark navy theme
# ---------------------------------------------------------------------------
CUSTOM_CSS = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* { font-family: 'Inter', sans-serif; }
/* Main background */
.gradio-container { background: #f0f2f5; max-width: 1200px; margin: 0 auto; }
/* Header banner */
.egm-header {
background: linear-gradient(135deg, #0a2342 0%, #1a3a5c 60%, #1e5280 100%);
color: white;
padding: 28px 32px 22px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(10,35,66,0.25);
}
.egm-header * { color: #ffffff !important; }
.egm-header h1 { font-size: 24px; font-weight: 700; margin: 0 0 4px; letter-spacing: -0.3px; color: #ffffff !important; }
.egm-header p { font-size: 13px; opacity: 0.75; margin: 0; font-weight: 400; color: #ffffff !important; }
.egm-badges { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.egm-badge {
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 20px;
padding: 3px 12px;
font-size: 11px;
font-weight: 500;
color: rgba(255,255,255,0.9);
}
/* Tabs */
.tab-nav button {
font-weight: 600;
font-size: 13px;
color: #4a5568;
border-radius: 8px 8px 0 0;
}
.tab-nav button.selected {
color: #0a2342;
border-bottom: 3px solid #0a2342;
background: white;
}
/* Chat bubbles β Gradio 5 selectors */
.message.user > div, .user > .bubble-wrap, [data-testid="user"] .prose,
.message.user .prose, div.user { background: #0a2342 !important; color: #ffffff !important; border-radius: 14px 14px 4px 14px !important; }
.message.user > div *, .message.user p, .message.user span { color: #ffffff !important; }
.message.bot > div, .bot > .bubble-wrap, [data-testid="bot"] .prose,
.message.bot .prose { background: white !important; border: 1px solid #e2e8f0 !important; border-radius: 14px 14px 14px 4px !important; box-shadow: 0 1px 4px rgba(0,0,0,0.06) !important; }
/* Input box */
textarea, input[type=text] {
border: 1.5px solid #cbd5e0;
border-radius: 10px;
background: white;
font-size: 14px;
transition: border-color 0.2s;
}
textarea:focus, input[type=text]:focus { border-color: #0a2342; box-shadow: 0 0 0 3px rgba(10,35,66,0.08); }
/* Buttons */
.gr-button-primary {
background: #0a2342;
border: none;
border-radius: 10px;
font-weight: 600;
letter-spacing: 0.2px;
transition: background 0.2s;
}
.gr-button-primary:hover { background: #1a3a5c; }
.gr-button-secondary { border-color: #cbd5e0; border-radius: 10px; font-weight: 500; }
/* Panels / blocks */
.gr-panel, .gr-box { border-radius: 12px; border: 1px solid #e2e8f0; box-shadow: 0 1px 6px rgba(0,0,0,0.05); }
/* Slider */
.gr-slider input[type=range]::-webkit-slider-thumb { background: #0a2342; }
/* Dropdown */
.gr-dropdown { border-radius: 10px; }
/* Footer */
.egm-footer { font-size: 11px; color: #94a3b8; text-align: center; padding: 12px 0 4px; }
/* Ensure bold text inside navy cards stays white */
.egm-card b, .egm-card strong { color: #ffffff; }
"""
with gr.Blocks(title="EGM Research Assistant", theme=gr.themes.Base(), css=CUSTOM_CSS) as demo:
gr.HTML(f"""
<div class="egm-header">
<h1>π EGM Research Assistant</h1>
<p>Evidence Gap Map Β· AI-powered research synthesis Β· Dataset-only responses</p>
<div class="egm-badges">
<span class="egm-badge">π {TOTAL} studies</span>
<span class="egm-badge">π Semantic RAG</span>
<span class="egm-badge">β
Dataset-only responses</span>
<span class="egm-badge">π Evidence visualised</span>
</div>
</div>
""")
with gr.Tabs():
# ββ Tab 1: Chat ββββββββββββββββββββββββββββββββββββββββββββββββββ
with gr.TabItem("π¬ Research Chat"):
with gr.Row():
with gr.Column(scale=4):
chatbot = gr.Chatbot(label="Research conversation",
height=480, type="messages")
with gr.Row():
msg = gr.Textbox(
placeholder="e.g. What does the evidence say about cash transfers on education outcomes?",
label="Your question", lines=2, scale=5)
send_btn = gr.Button("Send β", variant="primary", scale=1)
with gr.Column(scale=1, min_width=220):
gr.Markdown("### βοΈ Settings")
top_k = gr.Slider(minimum=10, maximum=358, value=25, step=5,
label="Studies retrieved per query",
info="Charts show ONLY retrieved studies β lower = more focused on your question")
clear_btn = gr.Button("π Clear", variant="secondary")
gr.Markdown("### π‘ Try asking")
gr.Examples(examples=[
["What interventions improve employment outcomes?"],
["Evidence on cash transfers for education in Sub-Saharan Africa"],
["How effective is microfinance for women's empowerment?"],
["Which studies focus on fragile or conflict-affected states?"],
["What qualitative studies exist in the dataset?"],
], inputs=msg)
chat_chart = gr.HTML(value=EMPTY_HTML, label="Evidence snapshot")
send_btn.click(fn=chat, inputs=[msg, chatbot, top_k],
outputs=[chatbot, chat_chart]).then(lambda: "", outputs=msg)
msg.submit(fn=chat, inputs=[msg, chatbot, top_k],
outputs=[chatbot, chat_chart]).then(lambda: "", outputs=msg)
clear_btn.click(fn=clear_chat, outputs=[chatbot, chat_chart])
# ββ Tab 2: Dataset Summary ββββββββββββββββββββββββββββββββββββββββ
with gr.TabItem("π Dataset Summary"):
# Load Plotly JS once β shared by all charts in this tab
gr.Markdown("""
### Explore the spread of any column in the dataset
Select a column below to instantly see how studies are distributed across categories.
No AI involved β this reads directly from the dataset.
""")
col_dropdown = gr.Dropdown(
choices=list(SUMMARY_COLS.keys()),
value="PSE Intervention Group",
label="Select column to summarise",
interactive=True,
)
summary_chart = gr.HTML(
value=make_summary_chart("Intervention Category"),
label="Column distribution"
)
col_dropdown.change(
fn=make_summary_chart,
inputs=col_dropdown,
outputs=summary_chart,
)
# ββ Tab 3: Evidence Gaps βββββββββββββββββββββββββββββββββββββββββ
with gr.TabItem("π΄ Evidence Gaps"):
gr.Markdown("""
### Evidence Gap Analysis
Heatmaps showing where evidence exists and where it is missing. Cells show study count or **GAP** where no evidence exists.
π΄ Red = no studies | π‘ Amber = limited (<5) | π΅ Navy = strong (10+)
""")
with gr.Row():
gap_type = gr.Radio(
choices=[
"Intervention Category Γ Outcome Category",
"Intervention Category Γ SDG",
"Outcome Category Γ SDG",
"PSE Intervention Γ PSE Outcome",
"PSE Intervention Γ SDG",
"PSE Outcome Γ SDG",
],
value="Intervention Category Γ Outcome Category",
label="Analysis type",
interactive=True,
)
gap_method = gr.Radio(
choices=["All", "IE Quantitative", "IE Qualitative", "SR (Systematic Review)"],
value="All",
label="Filter by method type",
interactive=True,
)
gap_output = gr.HTML(
value=make_gap_tab_content("Intervention Category Γ Outcome Category", "All")
)
gap_type.change(fn=make_gap_tab_content, inputs=[gap_type, gap_method], outputs=gap_output)
gap_method.change(fn=make_gap_tab_content, inputs=[gap_type, gap_method], outputs=gap_output)
gr.HTML('<div class="egm-footer">Responses grounded exclusively in the EGM dataset Β· sentence-transformers + ChromaDB + Claude </div>')
if __name__ == "__main__":
demo.launch()
|