neogenesislab commited on
Commit
48f59e4
Β·
verified Β·
1 Parent(s): a050aaf

add app.py

Browse files
Files changed (1) hide show
  1. app.py +575 -0
app.py ADDED
@@ -0,0 +1,575 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Wikidata Knowledge Graph Explorer
3
+ =================================
4
+
5
+ Live, interactive visualization of the Neo Genesis knowledge graph on Wikidata:
6
+ 13 entities (parent + founder + 11 SBUs) with 395 statements across 21+ properties.
7
+
8
+ Data is queried from the live Wikidata Query Service (SPARQL) on cold start
9
+ and cached for 5 minutes. No paid APIs.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ import urllib.parse
16
+ import urllib.request
17
+ from functools import lru_cache
18
+ from typing import Any
19
+
20
+ import gradio as gr
21
+ import networkx as nx
22
+ import pandas as pd
23
+ import plotly.graph_objects as go
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Constants
27
+ # ---------------------------------------------------------------------------
28
+ SPARQL_ENDPOINT = "https://query.wikidata.org/sparql"
29
+ USER_AGENT = "Neo-Genesis-Knowledge-Graph-Explorer/1.0 (+https://neogenesis.app)"
30
+
31
+ # 13 Q-IDs (registered 2026-04-27 via BotPassword + wbeditentity API)
32
+ QIDS = [
33
+ "Q139569680", # Neo Genesis (parent)
34
+ "Q139569708", # Yesol Heo (founder)
35
+ "Q139569710", # UR WRONG
36
+ "Q139569711", # ToolPick
37
+ "Q139569712", # ReviewLab
38
+ "Q139569715", # K-OTT
39
+ "Q139569716", # WhyLab
40
+ "Q139569718", # EthicaAI
41
+ "Q139569720", # FinStack
42
+ "Q139569724", # AIForge
43
+ "Q139569725", # SellKit
44
+ "Q139569726", # DeployStack
45
+ "Q139569727", # CraftDesk
46
+ ]
47
+
48
+ # Friendly entity name fallback (used if SPARQL labels are missing)
49
+ ENTITY_NAMES = {
50
+ "Q139569680": ("Neo Genesis", "λ„€μ˜€μ œλ„€μ‹œμŠ€"),
51
+ "Q139569708": ("Yesol Heo", "ν—ˆμ˜ˆμ†”"),
52
+ "Q139569710": ("UR WRONG", "μœ μ•Œλ‘±"),
53
+ "Q139569711": ("ToolPick", "νˆ΄ν”½"),
54
+ "Q139569712": ("ReviewLab", "리뷰랩"),
55
+ "Q139569715": ("K-OTT", "μΌ€μ΄μ˜€ν‹°ν‹°"),
56
+ "Q139569716": ("WhyLab", "μ™€μ΄λž©"),
57
+ "Q139569718": ("EthicaAI", "에티카AI"),
58
+ "Q139569720": ("FinStack", "ν•€μŠ€νƒ"),
59
+ "Q139569724": ("AIForge", "에이아이포지"),
60
+ "Q139569725": ("SellKit", "μ…€ν‚·"),
61
+ "Q139569726": ("DeployStack", "λ””ν”Œλ‘œμ΄μŠ€νƒ"),
62
+ "Q139569727": ("CraftDesk", "ν¬λž˜ν”„νŠΈλ°μŠ€ν¬"),
63
+ }
64
+
65
+ # Property friendly names
66
+ PROPERTY_NAMES = {
67
+ "P31": "instance of",
68
+ "P159": "headquarters location",
69
+ "P17": "country",
70
+ "P571": "inception date",
71
+ "P856": "official website",
72
+ "P1830": "owner of",
73
+ "P127": "owned by",
74
+ "P361": "part of",
75
+ "P1813": "short name",
76
+ "P1448": "official name",
77
+ "P3320": "board member",
78
+ "P1056": "product or material",
79
+ "P137": "operator",
80
+ "P1451": "motto",
81
+ "P21": "sex or gender",
82
+ "P27": "country of citizenship",
83
+ "P176": "manufacturer",
84
+ "P136": "genre",
85
+ "P452": "industry",
86
+ "P407": "language of work",
87
+ "P106": "occupation",
88
+ "P112": "founder",
89
+ "P1813": "short name",
90
+ "P2002": "Twitter username",
91
+ "P2013": "Facebook username",
92
+ "P2037": "GitHub username",
93
+ "P2003": "Instagram username",
94
+ "P973": "described at URL",
95
+ "P2888": "exact match",
96
+ "P2860": "cites work",
97
+ }
98
+
99
+
100
+ def _sparql(query: str, timeout: int = 30) -> dict[str, Any]:
101
+ url = SPARQL_ENDPOINT + "?" + urllib.parse.urlencode({"query": query, "format": "json"})
102
+ req = urllib.request.Request(
103
+ url,
104
+ headers={
105
+ "User-Agent": USER_AGENT,
106
+ "Accept": "application/sparql-results+json",
107
+ },
108
+ )
109
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
110
+ return json.loads(resp.read())
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Data loaders (cached 5 minutes)
115
+ # ---------------------------------------------------------------------------
116
+
117
+ def _bucket(seconds: int = 300) -> int:
118
+ """Cache bucket key β€” increments every ``seconds``."""
119
+ return int(time.time() // seconds)
120
+
121
+
122
+ @lru_cache(maxsize=4)
123
+ def load_entities(_cache: int) -> pd.DataFrame:
124
+ """Browse view: 13 entities with labels and statement counts."""
125
+ values = " ".join(f"wd:{q}" for q in QIDS)
126
+ query = f"""
127
+ SELECT ?item ?itemLabel ?itemLabel_ko ?type ?typeLabel ?statementCount WHERE {{
128
+ VALUES ?item {{ {values} }}
129
+ OPTIONAL {{ ?item rdfs:label ?itemLabel FILTER(LANG(?itemLabel)='en') }}
130
+ OPTIONAL {{ ?item rdfs:label ?itemLabel_ko FILTER(LANG(?itemLabel_ko)='ko') }}
131
+ OPTIONAL {{ ?item wdt:P31 ?type . ?type rdfs:label ?typeLabel FILTER(LANG(?typeLabel)='en') }}
132
+ {{
133
+ SELECT ?item (COUNT(?statement) AS ?statementCount) WHERE {{
134
+ ?item ?p ?statement .
135
+ FILTER(STRSTARTS(STR(?p), "http://www.wikidata.org/prop/P"))
136
+ }}
137
+ GROUP BY ?item
138
+ }}
139
+ }}
140
+ """
141
+ try:
142
+ data = _sparql(query)
143
+ except Exception as e:
144
+ print(f"WARN: load_entities SPARQL failed: {e}")
145
+ return _fallback_entities()
146
+
147
+ rows = []
148
+ seen = set()
149
+ for b in data.get("results", {}).get("bindings", []):
150
+ qid = b["item"]["value"].split("/")[-1]
151
+ if qid in seen:
152
+ continue
153
+ seen.add(qid)
154
+ en = b.get("itemLabel", {}).get("value", "")
155
+ ko = b.get("itemLabel_ko", {}).get("value", "")
156
+ type_label = b.get("typeLabel", {}).get("value", "")
157
+ sc = int(b.get("statementCount", {}).get("value", 0))
158
+ if not en and qid in ENTITY_NAMES:
159
+ en = ENTITY_NAMES[qid][0]
160
+ if not ko and qid in ENTITY_NAMES:
161
+ ko = ENTITY_NAMES[qid][1]
162
+ rows.append({
163
+ "Q-ID": qid,
164
+ "Label (en)": en,
165
+ "Label (ko)": ko,
166
+ "Type": type_label or "β€”",
167
+ "Statements": sc,
168
+ "URL": f"https://www.wikidata.org/wiki/{qid}",
169
+ })
170
+ rows.sort(key=lambda r: -r["Statements"])
171
+ return pd.DataFrame(rows)
172
+
173
+
174
+ def _fallback_entities() -> pd.DataFrame:
175
+ """Static fallback if SPARQL endpoint is rate-limited."""
176
+ rows = []
177
+ for qid in QIDS:
178
+ en, ko = ENTITY_NAMES.get(qid, ("", ""))
179
+ rows.append({
180
+ "Q-ID": qid,
181
+ "Label (en)": en,
182
+ "Label (ko)": ko,
183
+ "Type": "β€”",
184
+ "Statements": 0,
185
+ "URL": f"https://www.wikidata.org/wiki/{qid}",
186
+ })
187
+ return pd.DataFrame(rows)
188
+
189
+
190
+ @lru_cache(maxsize=64)
191
+ def load_entity_detail(qid: str, _cache: int) -> list[dict[str, str]]:
192
+ """Detail view: all statements for a given Q-ID, grouped by property."""
193
+ query = f"""
194
+ SELECT ?prop ?propLabel ?value ?valueLabel WHERE {{
195
+ wd:{qid} ?p ?statement .
196
+ ?prop wikibase:directClaim ?p .
197
+ ?statement ?ps ?value .
198
+ ?prop wikibase:claim ?ps .
199
+ OPTIONAL {{ ?prop rdfs:label ?propLabel FILTER(LANG(?propLabel)='en') }}
200
+ OPTIONAL {{ ?value rdfs:label ?valueLabel FILTER(LANG(?valueLabel)='en') }}
201
+ }}
202
+ ORDER BY ?prop
203
+ """
204
+ try:
205
+ data = _sparql(query)
206
+ except Exception as e:
207
+ print(f"WARN: load_entity_detail({qid}) SPARQL failed: {e}")
208
+ return []
209
+
210
+ rows = []
211
+ for b in data.get("results", {}).get("bindings", []):
212
+ prop_uri = b["prop"]["value"]
213
+ prop_id = prop_uri.split("/")[-1]
214
+ prop_label = b.get("propLabel", {}).get("value", "") or PROPERTY_NAMES.get(prop_id, prop_id)
215
+ val = b["value"]
216
+ val_uri = val.get("value", "")
217
+ if val_uri.startswith("http://www.wikidata.org/entity/Q"):
218
+ val_qid = val_uri.split("/")[-1]
219
+ val_label = b.get("valueLabel", {}).get("value", "") or val_qid
220
+ display = f"[{val_label}](https://www.wikidata.org/wiki/{val_qid})"
221
+ elif val_uri.startswith("http://") or val_uri.startswith("https://"):
222
+ display = f"[{val_uri}]({val_uri})"
223
+ else:
224
+ display = val_uri
225
+ rows.append({
226
+ "property_id": prop_id,
227
+ "property": prop_label,
228
+ "value": display,
229
+ })
230
+ return rows
231
+
232
+
233
+ @lru_cache(maxsize=4)
234
+ def load_relations(_cache: int) -> list[tuple[str, str, str]]:
235
+ """Graph view: P112/P361/P1830/P127 relationships among the 13 entities."""
236
+ values = " ".join(f"wd:{q}" for q in QIDS)
237
+ query = f"""
238
+ SELECT ?source ?prop ?target WHERE {{
239
+ VALUES ?source {{ {values} }}
240
+ VALUES ?target {{ {values} }}
241
+ VALUES ?prop {{ wdt:P112 wdt:P361 wdt:P1830 wdt:P127 wdt:P3320 }}
242
+ ?source ?prop ?target .
243
+ }}
244
+ """
245
+ try:
246
+ data = _sparql(query)
247
+ except Exception as e:
248
+ print(f"WARN: load_relations SPARQL failed: {e}")
249
+ return _fallback_relations()
250
+
251
+ edges = []
252
+ seen = set()
253
+ for b in data.get("results", {}).get("bindings", []):
254
+ src = b["source"]["value"].split("/")[-1]
255
+ tgt = b["target"]["value"].split("/")[-1]
256
+ prop = b["prop"]["value"].split("/")[-1]
257
+ # SPARQL endpoint sometimes returns wdt:* form; normalize to P-id
258
+ prop = prop.replace("statement/", "")
259
+ key = (src, prop, tgt)
260
+ if key in seen:
261
+ continue
262
+ seen.add(key)
263
+ edges.append(key)
264
+ if not edges:
265
+ return _fallback_relations()
266
+ return edges
267
+
268
+
269
+ def _fallback_relations() -> list[tuple[str, str, str]]:
270
+ """Heuristic: parent -> founder, parent -> all SBUs (P1830 owner of)."""
271
+ parent = "Q139569680"
272
+ founder = "Q139569708"
273
+ sbus = [q for q in QIDS if q not in (parent, founder)]
274
+ edges = [(parent, "P112", founder)]
275
+ edges += [(parent, "P1830", s) for s in sbus]
276
+ return edges
277
+
278
+
279
+ # ---------------------------------------------------------------------------
280
+ # Tab 1: Browse
281
+ # ---------------------------------------------------------------------------
282
+ def view_entities() -> pd.DataFrame:
283
+ return load_entities(_bucket())
284
+
285
+
286
+ # ---------------------------------------------------------------------------
287
+ # Tab 2: Entity Detail
288
+ # ---------------------------------------------------------------------------
289
+ def entity_detail_md(qid: str) -> str:
290
+ if not qid:
291
+ return "_Pick a Q-ID from the dropdown to see all statements._"
292
+ qid = qid.strip().split()[0] if " " in qid else qid.strip()
293
+ if not qid.startswith("Q"):
294
+ return f"_Invalid Q-ID: `{qid}`. Expected something like `Q139569680`._"
295
+
296
+ rows = load_entity_detail(qid, _bucket())
297
+ if not rows:
298
+ return (
299
+ f"_No statements found for [{qid}](https://www.wikidata.org/wiki/{qid}). "
300
+ f"This may be a transient SPARQL cache miss β€” try again in a few seconds._"
301
+ )
302
+
303
+ # Group by property
304
+ grouped: dict[str, list[dict[str, str]]] = {}
305
+ for r in rows:
306
+ grouped.setdefault(r["property"], []).append(r)
307
+
308
+ en, ko = ENTITY_NAMES.get(qid, ("", ""))
309
+ parts = [f"## [{qid}](https://www.wikidata.org/wiki/{qid}) β€” {en} / {ko}"]
310
+ parts.append(f"")
311
+ parts.append(f"**{len(rows)} statements** across **{len(grouped)} properties**.")
312
+ parts.append("")
313
+
314
+ for prop_label in sorted(grouped.keys()):
315
+ prop_rows = grouped[prop_label]
316
+ prop_id = prop_rows[0]["property_id"]
317
+ parts.append(
318
+ f"### [{prop_id}](https://www.wikidata.org/wiki/Property:{prop_id}) β€” {prop_label}"
319
+ )
320
+ for pr in prop_rows:
321
+ parts.append(f"- {pr['value']}")
322
+ parts.append("")
323
+ return "\n".join(parts)
324
+
325
+
326
+ def qid_choices() -> list[str]:
327
+ """Return human-friendly Q-ID dropdown choices."""
328
+ df = load_entities(_bucket())
329
+ out = []
330
+ for _, row in df.iterrows():
331
+ qid = row["Q-ID"]
332
+ en = row["Label (en)"] or ""
333
+ out.append(f"{qid} ({en})")
334
+ return out
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # Tab 3: Graph View
339
+ # ---------------------------------------------------------------------------
340
+ def build_graph_figure() -> go.Figure:
341
+ edges = load_relations(_bucket())
342
+ G = nx.DiGraph()
343
+ for q in QIDS:
344
+ en, ko = ENTITY_NAMES.get(q, (q, q))
345
+ G.add_node(q, label=en, label_ko=ko)
346
+ for src, prop, tgt in edges:
347
+ G.add_edge(src, tgt, prop=prop)
348
+
349
+ # Spring layout with parent at center
350
+ pos = nx.spring_layout(G, k=2.5, iterations=80, seed=42)
351
+
352
+ # Pin parent + founder positions for readability
353
+ if "Q139569680" in pos:
354
+ pos["Q139569680"] = (0, 0)
355
+ if "Q139569708" in pos:
356
+ pos["Q139569708"] = (1.5, 0.8)
357
+
358
+ edge_x, edge_y = [], []
359
+ for src, tgt in G.edges():
360
+ x0, y0 = pos[src]
361
+ x1, y1 = pos[tgt]
362
+ edge_x.extend([x0, x1, None])
363
+ edge_y.extend([y0, y1, None])
364
+
365
+ edge_trace = go.Scatter(
366
+ x=edge_x, y=edge_y,
367
+ line=dict(width=1, color="#888"),
368
+ hoverinfo="none",
369
+ mode="lines",
370
+ )
371
+
372
+ node_x, node_y, node_text, node_hover, node_size, node_color = [], [], [], [], [], []
373
+ for n in G.nodes():
374
+ x, y = pos[n]
375
+ node_x.append(x)
376
+ node_y.append(y)
377
+ en = G.nodes[n].get("label", n)
378
+ ko = G.nodes[n].get("label_ko", "")
379
+ node_text.append(en)
380
+ in_deg = G.in_degree(n)
381
+ out_deg = G.out_degree(n)
382
+ node_hover.append(
383
+ f"<b>{en}</b> / {ko}<br>"
384
+ f"Q-ID: {n}<br>"
385
+ f"In: {in_deg} | Out: {out_deg}<br>"
386
+ f"<a href='https://www.wikidata.org/wiki/{n}'>wikidata.org/wiki/{n}</a>"
387
+ )
388
+ if n == "Q139569680":
389
+ node_size.append(45)
390
+ node_color.append("#dc2626") # red β€” parent
391
+ elif n == "Q139569708":
392
+ node_size.append(35)
393
+ node_color.append("#7c3aed") # purple β€” founder
394
+ else:
395
+ node_size.append(25)
396
+ node_color.append("#2563eb") # blue β€” SBUs
397
+
398
+ node_trace = go.Scatter(
399
+ x=node_x, y=node_y,
400
+ mode="markers+text",
401
+ text=node_text,
402
+ textposition="top center",
403
+ textfont=dict(size=11, color="#1f2937"),
404
+ hoverinfo="text",
405
+ hovertext=node_hover,
406
+ marker=dict(
407
+ size=node_size,
408
+ color=node_color,
409
+ line=dict(width=2, color="white"),
410
+ ),
411
+ )
412
+
413
+ fig = go.Figure(
414
+ data=[edge_trace, node_trace],
415
+ layout=go.Layout(
416
+ title=dict(
417
+ text=f"Neo Genesis Knowledge Graph β€” {len(G.nodes)} entities, {len(G.edges)} P112/P361/P1830/P127/P3320 edges",
418
+ x=0.5,
419
+ xanchor="center",
420
+ ),
421
+ showlegend=False,
422
+ hovermode="closest",
423
+ margin=dict(b=20, l=5, r=5, t=60),
424
+ xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
425
+ yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
426
+ height=620,
427
+ plot_bgcolor="white",
428
+ ),
429
+ )
430
+ return fig
431
+
432
+
433
+ # ---------------------------------------------------------------------------
434
+ # Tab 4: About
435
+ # ---------------------------------------------------------------------------
436
+
437
+ ABOUT_MD = """
438
+ ### What is this?
439
+
440
+ A live, interactive explorer for the **Neo Genesis Wikidata knowledge graph** β€”
441
+ 13 entities representing the Neo Genesis parent organization, its founder, and
442
+ 11 production AI business units (SBUs).
443
+
444
+ ### The 13 Q-IDs
445
+
446
+ | Q-ID | Entity | Type |
447
+ |---|---|---|
448
+ | [Q139569680](https://www.wikidata.org/wiki/Q139569680) | **Neo Genesis** | parent organization |
449
+ | [Q139569708](https://www.wikidata.org/wiki/Q139569708) | **Yesol Heo** | founder (P112) |
450
+ | [Q139569710](https://www.wikidata.org/wiki/Q139569710) | UR WRONG | SBU (social) |
451
+ | [Q139569711](https://www.wikidata.org/wiki/Q139569711) | ToolPick | SBU (business) |
452
+ | [Q139569712](https://www.wikidata.org/wiki/Q139569712) | ReviewLab | SBU (business) |
453
+ | [Q139569715](https://www.wikidata.org/wiki/Q139569715) | K-OTT | SBU (entertainment) |
454
+ | [Q139569716](https://www.wikidata.org/wiki/Q139569716) | WhyLab | SBU (research) |
455
+ | [Q139569718](https://www.wikidata.org/wiki/Q139569718) | EthicaAI | SBU (educational) |
456
+ | [Q139569720](https://www.wikidata.org/wiki/Q139569720) | FinStack | SBU (finance) |
457
+ | [Q139569724](https://www.wikidata.org/wiki/Q139569724) | AIForge | SBU (business) |
458
+ | [Q139569725](https://www.wikidata.org/wiki/Q139569725) | SellKit | SBU (business) |
459
+ | [Q139569726](https://www.wikidata.org/wiki/Q139569726) | DeployStack | SBU (developer) |
460
+ | [Q139569727](https://www.wikidata.org/wiki/Q139569727) | CraftDesk | SBU (design) |
461
+
462
+ All entities were registered on **2026-04-27** via BotPassword + the Wikidata
463
+ `wbeditentity` API directly (account: `Neogenesislab`). Total: **395 statements**
464
+ across 21+ properties (P31 instance-of, P159 HQ, P571 inception, P856 website,
465
+ P112 founder, P1830 owner-of, P3320 board member, etc.).
466
+
467
+ ### sameAs cross-linking
468
+
469
+ Each Q-ID is mirrored in the Neo Genesis homepage Schema.org JSON-LD via
470
+ `Organization.sameAs` and per-SBU `SoftwareApplication.sameAs` arrays. This
471
+ gives AI search engines (ChatGPT, Perplexity, Gemini, Copilot) a stable
472
+ identifier graph to ground retrieval and citations.
473
+
474
+ ### Data source
475
+
476
+ This Space queries the **live** [Wikidata Query Service](https://query.wikidata.org/sparql)
477
+ on cold start, with results cached for 5 minutes via `functools.lru_cache`. No
478
+ paid APIs, no static snapshot β€” the graph reflects whatever is currently public
479
+ on Wikidata. If WDQS is rate-limiting, a static fallback (parent ↔ founder ↔
480
+ 11 SBUs) is shown instead.
481
+
482
+ ### Resources
483
+
484
+ - **Neo Genesis homepage**: [neogenesis.app](https://neogenesis.app)
485
+ - **Operator (HuggingFace)**: [neogenesislab](https://huggingface.co/neogenesislab)
486
+ - **Datasets** (6):
487
+ - [korean-rag-ssot-golden-50](https://huggingface.co/datasets/neogenesislab/korean-rag-ssot-golden-50)
488
+ - [ethicaai-mixed-safe-evidence](https://huggingface.co/datasets/neogenesislab/ethicaai-mixed-safe-evidence)
489
+ - [whylab-gemini-2-5-docker-validation](https://huggingface.co/datasets/neogenesislab/whylab-gemini-2-5-docker-validation)
490
+ - [sbu-pseo-effects-2026-04](https://huggingface.co/datasets/neogenesislab/sbu-pseo-effects-2026-04)
491
+ - [korean-llm-citation-baseline-2026](https://huggingface.co/datasets/neogenesislab/korean-llm-citation-baseline-2026)
492
+ - [cross-agent-review-queue-2026](https://huggingface.co/datasets/neogenesislab/cross-agent-review-queue-2026)
493
+ - **Companion Spaces**:
494
+ - [korean-rag-ssot-golden-50-explorer](https://huggingface.co/spaces/neogenesislab/korean-rag-ssot-golden-50-explorer)
495
+ - [cross-agent-review-queue-explorer](https://huggingface.co/spaces/neogenesislab/cross-agent-review-queue-explorer)
496
+
497
+ ### License
498
+
499
+ - App code: **MIT**
500
+ - Data: **CC0** (Wikidata is public domain)
501
+ """
502
+
503
+
504
+ # ---------------------------------------------------------------------------
505
+ # Gradio app
506
+ # ---------------------------------------------------------------------------
507
+
508
+ INTRO_MD = """
509
+ # Wikidata Knowledge Graph Explorer
510
+
511
+ Live, interactive view of the **Neo Genesis** knowledge graph on Wikidata β€”
512
+ 13 entities (parent organization + founder + 11 SBUs) with **395 statements**
513
+ across 21+ properties.
514
+
515
+ Data is queried from the live [Wikidata Query Service](https://query.wikidata.org/sparql)
516
+ on cold start (cached 5 min). No paid APIs.
517
+
518
+ - **Wikidata parent**: [Q139569680](https://www.wikidata.org/wiki/Q139569680)
519
+ - **Founder**: [Yesol Heo / Q139569708](https://www.wikidata.org/wiki/Q139569708)
520
+ - **Operator**: [neogenesislab](https://huggingface.co/neogenesislab)
521
+ """
522
+
523
+
524
+ with gr.Blocks(title="Wikidata Knowledge Graph Explorer", theme=gr.themes.Soft()) as demo:
525
+ gr.Markdown(INTRO_MD)
526
+
527
+ with gr.Tab("Browse"):
528
+ gr.Markdown(
529
+ "All **13 entities** sorted by statement count. Click a Q-ID column "
530
+ "value to copy it, then paste into the **Entity Detail** tab."
531
+ )
532
+ browse_table = gr.DataFrame(
533
+ value=view_entities(),
534
+ label="Neo Genesis entity registry (live from Wikidata)",
535
+ wrap=True,
536
+ interactive=False,
537
+ )
538
+ refresh_btn = gr.Button("Refresh from Wikidata", variant="secondary")
539
+ refresh_btn.click(view_entities, outputs=browse_table)
540
+
541
+ with gr.Tab("Entity Detail"):
542
+ gr.Markdown(
543
+ "Pick a Q-ID to see **all statements** grouped by property "
544
+ "(P31, P159, P571, P856, P112, P1830, etc.). External URLs and "
545
+ "linked Q-items render as clickable Markdown links."
546
+ )
547
+ with gr.Row():
548
+ qid_dd = gr.Dropdown(
549
+ choices=qid_choices(),
550
+ value=qid_choices()[0] if qid_choices() else "",
551
+ label="Q-ID",
552
+ scale=4,
553
+ )
554
+ view_btn = gr.Button("Show statements", variant="primary", scale=1)
555
+ detail_md = gr.Markdown(entity_detail_md(QIDS[0]))
556
+ view_btn.click(entity_detail_md, inputs=qid_dd, outputs=detail_md)
557
+ qid_dd.change(entity_detail_md, inputs=qid_dd, outputs=detail_md)
558
+
559
+ with gr.Tab("Graph View"):
560
+ gr.Markdown(
561
+ "Force-directed layout of P112 (founder), P361 (part of), P1830 "
562
+ "(owner of), P127 (owned by), and P3320 (board member) "
563
+ "relationships across the 13 entities. Hover any node to see "
564
+ "in/out degree and a Wikidata link."
565
+ )
566
+ graph_plot = gr.Plot(value=build_graph_figure())
567
+ graph_refresh = gr.Button("Re-query Wikidata", variant="secondary")
568
+ graph_refresh.click(build_graph_figure, outputs=graph_plot)
569
+
570
+ with gr.Tab("About"):
571
+ gr.Markdown(ABOUT_MD)
572
+
573
+
574
+ if __name__ == "__main__":
575
+ demo.queue().launch()