Spaces:
Running
Running
File size: 9,188 Bytes
78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 3d34953 78f2057 | 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 | """Knowledge Graph — the whole database as one graph, always current.
agent/kg.py reads every property row (rows in the review queue included, and
marked as such) and every harvested figure and builds the graph; this page
shows it in an explorer (agent/kg_explorer.html) and offers the same data for
download. Nothing is cached beyond a fingerprint check: when the tables change
(a cycle ingests papers, a reviewer promotes a row, routes are added), the
next view or the scheduler's refresh job rebuilds the graph.
The explorer loads its data from /app/static/kg/ when Streamlit's static file
serving is on and the files could be written (the Dockerfile makes that
directory writable): the graph, the per-value detail files (quoted sentence,
flag reason, date, model) it fetches when a value is opened, and the figure
pictures. Otherwise the data is embedded in the page, so the page works either
way; only the figure pictures need the static files.
"""
import os
import streamlit as st
from agent import kg, ui_theme as T
from agent.ui_common import status_banner
EXPLORER_HEIGHT = 880
INLINE_DETAILS_MAX = 6_000_000 # bytes of compressed value details embedded when there are no static files
# How often the open page asks whether the graph was rebuilt (kg-meta.json).
try:
POLL_SECONDS = int(os.environ.get("AGENT_KG_POLL_SECONDS", "") or 60)
except ValueError:
POLL_SECONDS = 60
# The explorer is a three-pane workbench: give this page more width than the
# 1360 px the other pages use.
st.html("""<style>
[data-testid="stMainBlockContainer"] { max-width: 1800px; }
</style>""")
T.page_header("Knowledge Graph",
"Every material, property, source document, figure and processing route "
"in the database as one graph, the review queue included. It is rebuilt "
"from the live tables whenever they change.")
status_banner()
try:
snap = kg.current()
except Exception as exc: # the database answered the banner but the build failed
st.error(f"The knowledge graph could not be built: {exc}")
st.stop()
c = snap.counts
if not c.get("rows"):
T.empty_state("🕸️", "The database is empty so far",
"The graph appears here as soon as the agent has ingested its "
"first document, and grows with every cycle.")
st.stop()
@st.fragment(run_every=POLL_SECONDS)
def _tiles() -> None:
"""The counts, re-read on a timer so they follow the database like the
explorer below does (which swaps in a new graph on its own)."""
try:
now = kg.current()
except Exception:
now = snap
k = now.counts
n_proc = int(k.get("rows_with_process") or 0)
T.kpi_row([
{"label": "Materials", "value": k["materials"],
"help": "Distinct materials across Polymers, Fibers and Composites"},
{"label": "Properties", "value": k["properties"],
"help": "Property names after merging case, spelling and listed synonyms"},
{"label": "Source documents", "value": k["sources"],
"delta": (f"{k['figures']:,} figures · {k.get('figures_with_values', 0):,} with values"
if k.get("figures") else None),
"delta_kind": "flat",
"help": "Figures harvested from the documents. Values read off a figure, and "
"values whose sentence cites one, link to it; the picture opens with it."},
{"label": "Measured values", "value": k["rows"],
"delta": f"{k['verified']:,} verified · {k['flagged']:,} in review",
"delta_kind": "flat",
"help": "Every property row in the database is in the graph. Rows in the review "
"queue are marked with the reason they were flagged; the switch above "
"the graph shows all values, the verified ones, or only the queue."},
{"label": "With processing route", "value": n_proc,
"delta": (f"{100 * n_proc / k['rows']:.0f}% of values" if k["rows"] else None),
"delta_kind": "flat",
"help": "Values that carry the process type and conditions of their "
"specimen (extracted since prompt 2.1)."},
{"label": "Rebuilt", "value": now.built_at.strftime("%H:%M UTC"),
"delta": now.built_at.strftime("%d %b %Y") + f" · {now.seconds:.1f} s",
"delta_kind": "flat",
"help": "The graph is rebuilt from the live tables whenever their row "
"counts, verified counts or newest extraction time change: right "
"after each cycle, and on a timer in between."},
])
_tiles()
def _static_served() -> bool:
try:
return bool(snap.static) and bool(st.get_option("server.enableStaticServing"))
except Exception:
return False
def _embed(html: str, height: int) -> None:
"""st.iframe where it exists (Streamlit >= 1.50); components.v1.html before."""
if hasattr(st, "iframe"):
try:
st.iframe(html, height=height)
return
except Exception:
pass
import streamlit.components.v1 as components
components.html(html, height=height, scrolling=True)
served = _static_served()
if served:
ver = "?v=" + snap.version
page = kg.explorer_html(data_url=snap.static["json"] + ver, gz_url=snap.static["gz"] + ver,
meta_url=snap.static["meta"], detail_base=snap.static["base"],
fig_base=snap.static["fig"], poll_seconds=POLL_SECONDS,
theme="dark" if T.is_dark() else "light", live=True, embedded=True)
else:
# No static files: embed the graph, and the value details too while they are
# small enough to send with the page. Figure pictures are not available then.
small = sum(len(b) for b in snap.details) <= INLINE_DETAILS_MAX
page = kg.explorer_html(inline_gz=snap.gz,
inline_details_gz=snap.details_gz() if small else None,
theme="dark" if T.is_dark() else "light", live=True, embedded=True)
_embed(page, EXPLORER_HEIGHT)
with st.container(border=True):
T.card_title("Use the graph elsewhere", "regenerated with every rebuild")
stamp = snap.built_at.strftime("%Y%m%d_%H%M")
host = os.environ.get("SPACE_HOST", "").strip()
base = f"https://{host}" if host else ""
left, right = st.columns([3, 2])
with left:
if served:
url_json, url_gz = base + snap.static["json"], base + snap.static["gz"]
st.html(
'<div class="aim-note">The explorer data is published as a file that always '
'matches the database:<br>'
f'<a href="{T.esc(snap.static["json"])}" target="_blank" rel="noopener">'
f'{T.esc(url_json)}</a> ({len(snap.json) / 1e6:.1f} MB) · '
f'<a href="{T.esc(snap.static["gz"])}" target="_blank" rel="noopener">'
f'compressed</a> ({len(snap.gz) / 1e6:.1f} MB) · '
f'<a href="{T.esc(snap.static["meta"])}" target="_blank" rel="noopener">'
f'counts and build time</a><br>The quoted sentence, flag reason, model and '
f'prompt of each value are in {len(snap.details):,} detail files next to it '
f'(the folder is named in the file, under <code>detail.dir</code>), and the '
f'figure pictures are at <code>{T.esc(base + snap.static["fig"])}'
f'<figure id>.png</code>.</div>')
else:
st.download_button("Graph data (JSON)", data=snap.json,
file_name=f"aim_kg_{stamp}.json", mime="application/json",
key="kg_dl_json")
st.html('<div class="aim-note">Materials, properties, sources and values come from the '
'database as stored. Polymer and fiber <b>families</b> are assigned here by '
'pattern rules, and property names are merged only for case, spelling and a '
'short synonym list (agent/kg.py); expect some wrong assignments at the edges. '
'The map is computed from names and counts, not simulated: the same database '
'always gives the same picture, and it shifts only where families grow.</div>')
with right:
if st.session_state.get("kg_neo4j_for") == snap.version:
st.download_button("Download nodes.csv + relationships.csv (zip)",
data=snap.neo4j_zip(), file_name=f"aim_kg_neo4j_{stamp}.zip",
mime="application/zip", key="kg_dl_neo4j")
elif st.button("Prepare the Neo4j export", key="kg_prep_neo4j",
help="One Measurement node per value, linked to its material, "
"property, source, figure and processing route, with its quoted "
"sentence, date and review-queue mark; with a load script."):
st.session_state["kg_neo4j_for"] = snap.version
st.rerun()
st.html('<div class="aim-note">For Neo4j, Memgraph, NetworkX or pandas: the full graph '
'with one node per measured value.</div>')
|