#3660 Neo4j Knowledge Graph GUI

#3660 Neo4j Knowledge Graph GUI

#3660 What Python library is best for creating an interactive GUI for creating and maintaining a knowledge graph using Python and Neo4j?

If you're aiming to build an interactive Python GUI for creating and maintaining a knowledge graph with Neo4j, here are some excellent options depending on your workflow, deployment environment, and goals:


1. neo4j-viz (Python wrapper for Neo4j’s Visualization Library)

  • What: A Python package that wraps the Neo4j Visualization Library (NVL), allowing interactive graph visualization in Jupyter Notebooks and Streamlit apps.

  • Features:

    • Supports node sizing, styling, captions, tooltips, pinning, zooming, panning, different layouts, and more

    • Create visualizations directly from Neo4j query results, GDS projections, or pandas DataFrames

    • Renders as IPython.display.HTML for seamless notebook integration Stack Overflow+4GitHub+4Graph Database & Analytics+4

  • Best for: Exploratory data work and rapid prototyping in notebook or lightweight web app environments.


2. ipysigma (Interactive Notebook Graph Visualization)

  • What: A Python library for rendering graph visualizations inside Jupyter notebooks, powered by Sigma.js.

  • Features:

    • Supports pan, zoom, hover, dynamic styling

    • Works with Neo4j or NetworkX graphs (though you’ll need to transform Neo4j results) Graph Database & Analytics

  • Best for: Interactive notebook exploration, especially for small to medium-size graphs.


3. Pyvis + VisJS


4. NeoDash

  • What: A Neo4j Labs tool for quickly building interactive dashboards that incorporate graph visualizations alongside charts and tables.

  • Features:

    • Low-code interface for building dashboards from Cypher query results

    • Mix graph visuals, bar charts, markdown, input parameters; output dashboards for sharing Graph Database & Analytics+1

  • Best for: Non-programmer friendly, dashboard-style interfaces for knowledge graph monitoring.


5. Linkurious (Commercial)

  • What: A powerful commercial platform for visualizing and exploring Neo4j (and other graph DBs) using a sleek web-based interface.

  • Features:

  • Best for: Enterprise environments demanding robust, polished, and supported visualization tools.


6. General-Purpose Python GUI Frameworks

  • What: Use frameworks like Bokeh, Panel, or Holoviz to build custom interactive apps integrating Neo4j graph visuals.

  • Features:

    • Bokeh + Panel offer Pythonic control and web deployment options for charts and widgets discuss.python.org

  • Best for: Fully custom GUI workflows combining graph visualizations with rich interactive controls.


Feature Comparison

Tool / LibraryIntegration StyleInteractive FeaturesBest For...
neo4j-vizNotebook / StreamlitZoom, pan, tooltips, styling, layoutsRapid prototyping & interactive exploration
ipysigmaJupyter NotebookPan, zoom, hover, stylingNotebook-based exploratory visualizations
Pyvis (VisJS)Web / NotebookPhysics layouts, tooltips, stylingLightweight custom web/displays
NeoDashDashboard (low-code)Composite visuals + widgetsInteractive dashboards from Neo4j
LinkuriousWeb App (commercial)Advanced UI, case workflowsEnterprise-grade graph visualization
Bokeh / PanelCustom Python web GUICharts, widgets, control logicCustom GUI integrating with Neo4j data

Recommendations by Use Case

  • Exploration / Prototyping:
    Use neo4j-viz or ipysigma if you're already working in notebooks and want interactivity built-in.

  • Custom Light Web UI:
    Pyvis, Bokeh, or Panel are flexible options where you'd code your own GUI logic and visuals.

  • Dashboard or Non-Developer Interface:
    NeoDash offers low-code construction of interactive dashboards ideal for sharing and quick visual oversight.

  • Enterprise & Professional Needs:
    If budget and requirements allow, Linkurious gives polished, supported interfaces built for real-world investigative use.


Final Thoughts

If you want a pure Python experience with interactive graph visuals for development or embedded apps, the combination of neo4j-viz for rendering rich visuals and something like Bokeh/Panel for UI logic gives a clean developer stack.

Alternatively, NeoDash provides that functionality with minimal coding, and Linkurious brings enterprise readiness when scale and polish are priorities.


Feel free to share more context—like environment (notebook vs standalone app), target users (developers vs analysts vs end-users), or dataset scale—and I’d be happy to help tailor the perfect stack.


What about for adding nodes, selecting files for emedding, and queries?

Great follow-up. For adding nodes/edges, uploading files to embed, and running queries from a Python GUI, the most productive stacks I recommend are:

Best all-around (fast to build, easy to ship)

Streamlit + Neo4j Python Driver + a graph widget

  • Why: dead-simple forms & file uploads, session state, quick auth, deploy anywhere.

  • Graph widgets:

    • streamlit-cytoscapejs (stable, interactive) or

    • streamlit-agraph / pyvis (vis.js under the hood).

  • Embeddings: sentence-transformers (local) or OpenAI embeddings.

  • Perfect for: CRUD panels (add/update/delete nodes/relationships), file chooser for embedding, and an ad-hoc Cypher console.

For a richer web app with fine-grained graph editing

Plotly Dash + dash-cytoscape

  • Why: industrial-strength callbacks; cytoscape supports selecting, adding, deleting, dragging, styling; file uploads with dcc.Upload.

  • Perfect for: power-user graph editors and multi-page apps.

Notebook-centric prototyping

Jupyter + ipywidgets + ipycytoscape

  • Why: fast experiments; OK for CRUD, uploads, and Cypher, less ideal for end users.


What this looks like (Streamlit pattern)

1) Add nodes/relationships (CRUD)

# streamlit_app.py import streamlit as st from neo4j import GraphDatabase URI = st.secrets["NEO4J_URI"] AUTH = (st.secrets["NEO4J_USER"], st.secrets["NEO4J_PASS"]) driver = GraphDatabase.driver(URI, auth=AUTH) def run_cypher(tx, q, **params): return list(tx.run(q, **params)) st.title("KG Admin") with driver.session() as sess: st.subheader("Add Node") col1, col2 = st.columns(2) with col1: label = st.text_input("Label", value="Document") name = st.text_input("name", "") with col2: props = st.text_area("Extra JSON props (optional)", value='{"source":"manual"}') if st.button("Create Node"): import json p = {"name": name, **json.loads(props or "{}")} q = f"CREATE (n:{label} $props) RETURN n" res = sess.execute_write(run_cypher, q, props=p) st.success(f"Created {label}('{name}')") st.subheader("Add Relationship") a_lbl = st.text_input("From Label", "Document") a_key = st.text_input("From name", "") rel = st.text_input("REL TYPE", "HAS_CHUNK") b_lbl = st.text_input("To Label", "Doc_Chunk") b_key = st.text_input("To id", "") if st.button("Create Relationship"): q = f""" MATCH (a:{a_lbl} {{name:$a}}), (b:{b_lbl} {{id:$b}}) MERGE (a)-[r:{rel}]->(b) RETURN type(r) AS rel """ res = sess.execute_write(run_cypher, q, a=a_key, b=b_key) st.success(f"Linked {a_lbl}('{a_key}') -[:{rel}]-> {b_lbl}('{b_key}')")

2) Select files and embed into Neo4j

import streamlit as st from sentence_transformers import SentenceTransformer from neo4j import GraphDatabase import uuid, io model = SentenceTransformer("all-MiniLM-L6-v2") upload = st.file_uploader("Select files to embed", type=["txt","md","pdf"]) def chunk_text(text, size=800, overlap=120): words = text.split() chunks, i = [], 0 while i < len(words): chunk = " ".join(words[i:i+size]) chunks.append(chunk) i += size - overlap return chunks if upload and st.button("Embed & Ingest"): raw = upload.read() if upload.type == "application/pdf": # minimalistic pdf extraction (replace with pymupdf / pdfminer.six for quality) import pdfminer.high_level text = pdfminer.high_level.extract_text(io.BytesIO(raw)) else: text = raw.decode("utf-8", errors="ignore") chunks = chunk_text(text) embs = model.encode(chunks, convert_to_numpy=True) doc_id = str(uuid.uuid4()) doc_name = upload.name with GraphDatabase.driver(st.secrets["NEO4J_URI"], auth=(st.secrets["NEO4J_USER"], st.secrets["NEO4J_PASS"])).session() as sess: # create Document sess.run(""" MERGE (d:Document {id:$id}) SET d.name=$name, d.source=$src, d.chunk_count=$n """, id=doc_id, name=doc_name, src="upload", n=len(chunks)) # create chunks with embeddings (as list or as vector via Neo4j Vector index) for i,(c,e) in enumerate(zip(chunks, embs)): cid = str(uuid.uuid4()) sess.run(""" MERGE (c:Doc_Chunk {id:$cid}) SET c.text=$text, c.idx=$i, c.embed=$emb // requires APOC or vector index enabled WITH c MATCH (d:Document {id:$doc}) MERGE (d)-[:HAS_CHUNK]->(c) """, cid=cid, text=c, i=i, emb=e.tolist(), doc=doc_id) st.success(f"Ingested '{doc_name}' with {len(chunks)} chunks.")

Notes:
• If you’re on Neo4j 5.18+, use native vector indexes (CREATE VECTOR INDEX …) and store embeddings as LIST<FLOAT>.
• For PDFs, swap in PyMuPDF (fitz) for better extraction.
• For very large files, stream chunks and use execute_write batching.

3) A simple query console + result viewer

st.subheader("Cypher Console") query = st.text_area("Cypher", "MATCH (n) RETURN n LIMIT 25") if st.button("Run Query"): with driver.session() as sess: rows = sess.run(query) data = [r.data() for r in rows] st.write(data)

4) Interactive graph panel (one-liner drop-in)

With streamlit-cytoscapejs:

# pip install streamlit-cytoscapejs import streamlit as st from streamlit_cytoscapejs import cytoscape from neo4j import GraphDatabase with driver.session() as sess: res = sess.run(""" MATCH (a)-[r]->(b) RETURN labels(a)[0] AS a_lbl, a.id AS a_id, a.name AS a_name, type(r) AS rel, labels(b)[0] AS b_lbl, b.id AS b_id, b.name AS b_name LIMIT 500 """) elements = [] nodes_seen = set() for rec in res: a_key = rec["a_id"] or rec["a_name"] b_key = rec["b_id"] or rec["b_name"] if a_key not in nodes_seen: elements.append({"data":{"id":a_key,"label":f'{rec["a_lbl"]}:{rec["a_name"] or a_key}'}}) nodes_seen.add(a_key) if b_key not in nodes_seen: elements.append({"data":{"id":b_key,"label":f'{rec["b_lbl"]}:{rec["b_name"] or b_key}'}}) nodes_seen.add(b_key) elements.append({"data":{"source":a_key,"target":b_key,"label":rec["rel"]}}) cytoscape(elements, layout={"name":"cose"}, stylesheet=[])

You can wire tap events (node/edge clicks) to populate edit forms and commit MERGEs/DELETEs.


Alternatives at a glance

  • Dash + dash-cytoscape: most control for graph editing (add/delete nodes & edges via callbacks), plus dcc.Upload for files and a Cypher console. Great when you want a multi-page app with role-based UI.

  • NiceGUI (FastAPI under the hood): modern Python-only web UI, easy dialogs/file pickers, embed a cytoscape or vis.js component in a few lines.

  • Gradio: super fast prototypes for upload → embed → query workflows; less flexible for live graph editing.


Recommendation for you

Given your pace and Neo4j work, I’d start with Streamlit for speed, and add:

  1. CRUD forms for labels/properties/relationships,

  2. File uploader + embedding pipeline,

  3. Cypher console, and

  4. Cytoscape graph for visual editing.

If you outgrow it, port to Dash + dash-cytoscape for finer control.

If you want, I can package a minimal project scaffold (Streamlit + neo4j driver + embeddings + cytoscape) so you can run streamlit run app.py and have all three: add, embed, query.


Do it

# 🧰 Neo4j Knowledge Graph Admin – Streamlit Scaffold

A minimal, production‑ready scaffold for **adding/editing nodes & relationships**, **uploading files for embedding**, and **running Cypher queries** against **Neo4j** — all in a single **Streamlit** app with an interactive **Cytoscape** graph panel.

---

## 📁 Project layout

```

kg-admin/

├─ app.py

├─ requirements.txt

├─ README.md

├─ .streamlit/

│  └─ secrets.toml   # add your Neo4j creds here

└─ utils/

   ├─ __init__.py

   └─ embedding.py

```

---


## 🔐 .streamlit/secrets.toml (template)

```toml

# Create this file locally at: .streamlit/secrets.toml

NEO4J_URI = "neo4j+s://<hosted-instance-or-bolt-uri>"

NEO4J_USER = "neo4j"

NEO4J_PASS = "password"


# Optional: tweak sizes

CHUNK_SIZE = 800

CHUNK_OVERLAP = 120

EMBED_MODEL = "sentence-transformers/all-MiniLM-L6-v2"  # 384-dim

VECTOR_INDEX_NAME = "chunk_embed_index"

```

---


## 📦 requirements.txt

```txt

streamlit>=1.36

neo4j>=5.20

streamlit-cytoscapejs>=0.0.5

sentence-transformers>=3.0.1

numpy>=1.26

pandas>=2.0

pymupdf>=1.24.9

pdfminer.six>=20231228

python-dotenv>=1.0

```

---


## 🧠 utils/embedding.py

```python

from __future__ import annotations

import io

from typing import List, Tuple


import numpy as np


# PDF/text extraction

import fitz  # PyMuPDF

from pdfminer.high_level import extract_text as pdfminer_extract


from sentence_transformers import SentenceTransformer



def load_model(model_name: str = "sentence-transformers/all-MiniLM-L6-v2") -> SentenceTransformer:

    return SentenceTransformer(model_name)



def extract_text(file_bytes: bytes, mime: str) -> str:

    """Return plain text from uploaded file by MIME type."""

    if mime == "application/pdf":

        # Use PyMuPDF first for speed/quality; fallback to pdfminer if needed

        try:

            doc = fitz.open(stream=file_bytes, filetype="pdf")

            text = []

            for page in doc:

                text.append(page.get_text())

            return "\n".join(text)

        except Exception:

            return pdfminer_extract(io.BytesIO(file_bytes))

    # default: any text-like

    return file_bytes.decode("utf-8", errors="ignore")



def chunk_text(text: str, size: int = 800, overlap: int = 120) -> List[str]:

    words = text.split()

    if not words:

        return []

    chunks, i = [], 0

    step = max(1, size - overlap)

    while i < len(words):

        chunk = " ".join(words[i:i + size])

        chunks.append(chunk)

        i += step

    return chunks



def embed_chunks(model: SentenceTransformer, chunks: List[str]) -> np.ndarray:

    if not chunks:

        return np.zeros((0, 0), dtype=np.float32)

    emb = model.encode(chunks, convert_to_numpy=True, normalize_embeddings=True)

    return emb.astype("float32")



def prepare_embeddings(text: str, model: SentenceTransformer, size: int, overlap: int) -> Tuple[List[str], np.ndarray]:

    chunks = chunk_text(text, size=size, overlap=overlap)

    embs = embed_chunks(model, chunks)

    return chunks, embs

```

```

---


## 🚀 app.py

```python

from __future__ import annotations

import json

import uuid

from typing import Any, Dict, List


import numpy as np

import pandas as pd

import streamlit as st

from neo4j import GraphDatabase

from streamlit_cytoscapejs import cytoscape


from utils.embedding import load_model, extract_text, prepare_embeddings


# -------------------------

# Config / Secrets

# -------------------------

st.set_page_config(page_title="KG Admin", layout="wide")


URI = st.secrets.get("NEO4J_URI")

USER = st.secrets.get("NEO4J_USER")

PASS = st.secrets.get("NEO4J_PASS")

CHUNK_SIZE = int(st.secrets.get("CHUNK_SIZE", 800))

CHUNK_OVERLAP = int(st.secrets.get("CHUNK_OVERLAP", 120))

EMBED_MODEL = st.secrets.get("EMBED_MODEL", "sentence-transformers/all-MiniLM-L6-v2")

VECTOR_INDEX = st.secrets.get("VECTOR_INDEX_NAME", "chunk_embed_index")


if not all([URI, USER, PASS]):

    st.error("Please configure NEO4J_URI / NEO4J_USER / NEO4J_PASS in .streamlit/secrets.toml")

    st.stop()


# -------------------------

# Neo4j Driver Helpers

# -------------------------

@st.cache_resource(show_spinner=False)

def get_driver():

    return GraphDatabase.driver(URI, auth=(USER, PASS))



def run_query(query: str, params: Dict[str, Any] | None = None) -> List[Dict[str, Any]]:

    with get_driver().session() as sess:

        res = sess.run(query, params or {})

        return [r.data() for r in res]



def write_query(query: str, params: Dict[str, Any] | None = None) -> List[Dict[str, Any]]:

    with get_driver().session() as sess:

        res = sess.execute_write(lambda tx: list(tx.run(query, params or {})))

        return [r.data() for r in res]


# -------------------------

# Vector Index bootstrap

# -------------------------

VECTOR_DIM = 384  # for all-MiniLM-L6-v2


def ensure_vector_index():

    cypher = f"""

    CREATE VECTOR INDEX {VECTOR_INDEX} IF NOT EXISTS

    FOR (c:Doc_Chunk) ON (c.embed)

    OPTIONS {{ indexConfig: {{ `vector.dimensions`: {VECTOR_DIM}, `vector.similarity_function`: 'cosine' }} }}

    """

    write_query(cypher)


# -------------------------

# Sidebar – Connection & Tools

# -------------------------

with st.sidebar:

    st.title("KG Admin")

    st.caption("Neo4j + Streamlit scaffold")


    st.markdown("### Connection")

    if st.button("Test connection"):

        try:

            v = run_query("RETURN 1 AS ok")

            st.success(f"OK → {v}")

        except Exception as e:

            st.exception(e)


    st.markdown("---")

    st.markdown("### Setup vector index")

    if st.button("Ensure vector index"):

        try:

            ensure_vector_index()

            st.success("Vector index ensured.")

        except Exception as e:

            st.exception(e)


# -------------------------

# Tabs

# -------------------------

T1, T2, T3, T4 = st.tabs([

    "➕ Add/Edit", "📄 Upload & Embed", "🔎 Query Console", "🕸️ Graph"

])


# -------------------------

# Tab 1 – Add/Edit Nodes & Relationships

# -------------------------

with T1:

    st.subheader("Create / Update Node")

    colA, colB = st.columns(2)

    with colA:

        n_label = st.text_input("Label", value="Document")

        key_prop = st.text_input("Key property name", value="id")

        key_val = st.text_input("Key value", value=str(uuid.uuid4()))

    with colB:

        props_json = st.text_area("Additional Properties (JSON)", value='{"name":"Sample"}')


    if st.button("MERGE Node"):

        try:

            props = json.loads(props_json or "{}")

            props[key_prop] = key_val

            q = f"MERGE (n:{n_label} {{{key_prop}: $key}}) SET n += $props RETURN labels(n) AS labels, n AS node"

            data = write_query(q, {"key": key_val, "props": props})

            st.success("Node merged.")

            st.json(data)

        except Exception as e:

            st.exception(e)


    st.markdown("---")

    st.subheader("Create Relationship")


    c1, c2 = st.columns(2)

    with c1:

        a_label = st.text_input("From Label", value="Document")

        a_key_prop = st.text_input("From Key Prop", value="id")

        a_key_val = st.text_input("From Key Val", value="")

        rel_type = st.text_input("REL TYPE", value="HAS_CHUNK")

    with c2:

        b_label = st.text_input("To Label", value="Doc_Chunk")

        b_key_prop = st.text_input("To Key Prop", value="id")

        b_key_val = st.text_input("To Key Val", value="")

        rel_props_json = st.text_area("Rel Properties (JSON)", value='{}')


    if st.button("MERGE Relationship"):

        try:

            rprops = json.loads(rel_props_json or "{}")

            q = f"""

            MATCH (a:{a_label} {{{a_key_prop}: $a}}), (b:{b_label} {{{b_key_prop}: $b}})

            MERGE (a)-[r:{rel_type}]->(b)

            SET r += $rprops

            RETURN type(r) AS rel, r AS properties

            """

            data = write_query(q, {"a": a_key_val, "b": b_key_val, "rprops": rprops})

            st.success("Relationship merged.")

            st.json(data)

        except Exception as e:

            st.exception(e)


# -------------------------

# Tab 2 – Upload & Embed

# -------------------------

with T2:

    st.subheader("Upload a file → chunk → embed → ingest")

    up = st.file_uploader("Choose a file", type=["txt", "md", "pdf"])


    cs, co = st.columns(2)

    with cs:

        size = st.number_input("Chunk size (words)", 100, 4000, CHUNK_SIZE, 50)

    with co:

        overlap = st.number_input("Chunk overlap (words)", 0, 4000, CHUNK_OVERLAP, 10)


    model = load_model(EMBED_MODEL)


    if up and st.button("Embed & Ingest into Neo4j"):

        try:

            text = extract_text(up.read(), up.type)

            chunks, embs = prepare_embeddings(text, model, size=int(size), overlap=int(overlap))


            doc_id = str(uuid.uuid4())


            # Create Document + Chunks

            q_doc = """

            MERGE (d:Document {id:$id})

            SET d.name=$name, d.source=$src, d.chunk_count=$n

            RETURN d

            """

            write_query(q_doc, {"id": doc_id, "name": up.name, "src": "upload", "n": len(chunks)})


            # Ingest chunks in batches

            batch = []

            for i, (chunk, vec) in enumerate(zip(chunks, embs)):

                batch.append({

                    "cid": str(uuid.uuid4()),

                    "idx": i,

                    "text": chunk,

                    "emb": vec.tolist(),

                    "doc": doc_id,

                })


            # Use UNWIND for efficiency

            q_chunks = f"""

            UNWIND $rows AS row

            MERGE (c:Doc_Chunk {{id: row.cid}})

            SET c.idx = row.idx, c.text = row.text, c.embed = row.emb

            WITH c, row

            MATCH (d:Document {{id: row.doc}})

            MERGE (d)-[:HAS_CHUNK]->(c)

            RETURN count(*) AS upserts

            """

            write_query(q_chunks, {"rows": batch})


            st.success(f"Ingested {len(chunks)} chunks for '{up.name}' (doc_id={doc_id}).")

            st.info("If it's your first run, click ‘Ensure vector index’ in the sidebar.")

        except Exception as e:

            st.exception(e)


    st.markdown("---")

    st.subheader("Vector Search (Semantic)")

    qtext = st.text_input("Query text", value="sample search")

    topk = st.slider("Top K", 1, 50, 5)

    if st.button("Search"):

        try:

            qvec = load_model(EMBED_MODEL).encode([qtext], normalize_embeddings=True)[0].astype("float32").tolist()

            cy = f"""

            CALL db.index.vector.queryNodes('{VECTOR_INDEX}', $k, $qvec)

            YIELD node, score

            RETURN node.id AS id, node.idx AS idx, node.text AS text, score

            """

            rows = run_query(cy, {"k": int(topk), "qvec": qvec})

            df = pd.DataFrame(rows)

            st.dataframe(df, use_container_width=True)

        except Exception as e:

            st.exception(e)


# -------------------------

# Tab 3 – Query Console

# -------------------------

with T3:

    st.subheader("Cypher Console")

    default_q = """

    MATCH (n)

    RETURN labels(n) AS labels, n.id AS id, n.name AS name

    LIMIT 50

    """.strip()

    cy = st.text_area("Cypher", value=default_q, height=160)

    if st.button("Run"):

        try:

            rows = run_query(cy)

            st.dataframe(pd.DataFrame(rows), use_container_width=True)

        except Exception as e:

            st.exception(e)


# -------------------------

# Tab 4 – Graph View (Cytoscape)

# -------------------------

with T4:

    st.subheader("Interactive Graph")

    max_edges = st.slider("Max relationships to visualize", 50, 2000, 500, 50)


    cy = f"""

    MATCH (a)-[r]->(b)

    RETURN labels(a)[0] AS a_lbl, coalesce(a.id, a.name) AS a_id, coalesce(a.name, a.id) AS a_name,

           type(r) AS rel,

           labels(b)[0] AS b_lbl, coalesce(b.id, b.name) AS b_id, coalesce(b.name, b.id) AS b_name

    LIMIT $limit

    """

    rows = run_query(cy, {"limit": int(max_edges)})


    nodes = {}

    edges = []

    for rec in rows:

        akey = rec["a_id"]; bkey = rec["b_id"]

        if akey and akey not in nodes:

            nodes[akey] = {"data": {"id": akey, "label": f"{rec['a_lbl']}\n{rec['a_name']}"}}

        if bkey and bkey not in nodes:

            nodes[bkey] = {"data": {"id": bkey, "label": f"{rec['b_lbl']}\n{rec['b_name']}"}}

        edges.append({"data": {"source": akey, "target": bkey, "label": rec["rel"]}})


    elements = list(nodes.values()) + edges


    cytoscape(

        elements,

        layout={"name": "cose"},

        stylesheet=[

            {"selector": "node", "style": {"label": "data(label)", "font-size": 10, "text-wrap": "wrap", "text-max-width": 120}},

            {"selector": "edge", "style": {"label": "data(label)", "curve-style": "bezier", "target-arrow-shape": "vee"}},

        ],

        height="700px",

    )


    st.caption("Tip: Use the Add/Edit tab to modify nodes/edges; refresh this tab to see updates.")

```

---


## 📖 README.md

```markdown

# Neo4j Knowledge Graph Admin – Streamlit Scaffold


A minimal UI to 

**add/edit nodes & relationships**, 

**upload/embed documents**, and 

**run Cypher**

with a live **graph view**.


## Quick Start

1. **Python** 3.10+

2. `python -m venv .venv && source .venv/bin/activate` (Windows: `.venv\\Scripts\\activate`)

3. `pip install -r requirements.txt`

4. Create `.streamlit/secrets.toml` with your Neo4j credentials (see template).

5. (Optional) Click **Ensure vector index** in the sidebar on first run.

6. `streamlit run app.py`


## Notes

- Default embedding model = `all-MiniLM-L6-v2` (384‑dim). Adjust in `secrets.toml`.

- Embeddings stored as `LIST<FLOAT>` in `:Doc_Chunk(embed)`; uses **Neo4j native vector index**.

- For huge docs, raise chunk size carefully; prefer batching and server‑side limits.

- Secure prod creds via environment or Streamlit Cloud secrets; consider Neo4j roles.


## Common Cypher

Create vector index (if not using the sidebar button):

```cypher

CREATE VECTOR INDEX chunk_embed_index IF NOT EXISTS

FOR (c:Doc_Chunk) ON (c.embed)

OPTIONS {indexConfig: {`vector.dimensions`: 384, `vector.similarity_function`: 'cosine'}};

```

Vector search:

```cypher

CALL db.index.vector.queryNodes('chunk_embed_index', 5, $qvec)

YIELD node, score

RETURN node, score;

```

```

---


## ✅ Done

You can now:

- **Add nodes/edges** (MERGE forms)

- **Upload & embed** TXT/MD/PDF into `Document` → `Doc_Chunk` with vector index

- **Run Cypher** in‑app

- **Explore graph** interactively (Cytoscape)


If you want role‑based auth, multi‑page apps, or graph edit by clicks (tap to edit → MERGE), say the word and I’ll extend this scaffold. 

Boom — scaffold delivered. It’s in the canvas with:

  • a ready-to-run app.py (Streamlit)

  • upload → chunk → embed → Neo4j ingest

  • CRUD forms for nodes/relationships

  • a Cypher console

  • an interactive Cytoscape graph view

  • vector index bootstrap + semantic search

How to run

  1. create .streamlit/secrets.toml (use the template in the canvas)

  2. pip install -r requirements.txt

  3. streamlit run app.py

Want me to add click-to-edit on the graph (tap a node/edge → prefill the form), or role-based auth?


From <https://chatgpt.com/>  ChatGPT 5

Comments

Popular posts from this blog

#2892 Example of ChatGPT o1 Prompt

#3105 Federal and State Spending Idiocy