From b550a03d65af570f716efedcca4e1efeefb3b720 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 18:06:03 +0300 Subject: [PATCH 01/86] feat(client): add create_label/create_edge_type with SchemaMode; update proto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update proto submodule to 381dbcd: adds SchemaMode enum (STRICT/VALIDATED/ FLEXIBLE), ComputedPropertyDefinition, schema_mode field on CreateLabelRequest and Label messages - Add AsyncCoordinodeClient.create_label(name, properties, *, schema_mode) → LabelInfo with full _type_map / _mode_map conversion; schema_mode defaults to 'strict' - Add AsyncCoordinodeClient.create_edge_type(name, properties) → EdgeTypeInfo (proto does not expose schema_mode for edge types) - Add sync wrappers CoordinodeClient.create_label / create_edge_type - Expose schema_mode: int on LabelInfo and EdgeTypeInfo (getattr-safe: defaults to 0 for older server images that omit the field) - Revert refresh_schema() labels(a)[0] shortcut to labels(a) AS src_labels + Python _first_label() helper; published Docker image does not yet support subscript-on-function syntax (TODO comment added) - Add 11 unit tests (test_schema_crud.py): schema_mode propagation on LabelInfo and EdgeTypeInfo, missing-field default, repr coverage - Add 6 integration tests (test_sdk.py): create_label / create_edge_type round- trips, schema_mode=flexible acceptance, invalid schema_mode ValueError, appears-in-list verification (with documented workaround for G009) - Mark test_traverse_inbound as xfail: INBOUND direction not supported by server Known gaps recorded in GAPS.md: G008 - SchemaMode enforcement not active in published image G009 - ListLabels/ListEdgeTypes omits schema-only objects (no nodes yet) G010 - labels(n)[0] subscript not in published image (workaround: _first_label) G011 - traverse INBOUND direction unimplemented on server --- coordinode/coordinode/client.py | 138 +++++++++++++++++- .../langchain_coordinode/graph.py | 22 +-- proto | 2 +- tests/integration/test_sdk.py | 85 +++++++++++ tests/unit/test_schema_crud.py | 53 ++++++- uv.lock | 2 +- 6 files changed, 281 insertions(+), 21 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index b270dd1..ea96d04 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -105,9 +105,11 @@ def __init__(self, proto_label: Any) -> None: self.name: str = proto_label.name self.version: int = proto_label.version self.properties: list[PropertyDefinitionInfo] = [PropertyDefinitionInfo(p) for p in proto_label.properties] + # schema_mode: 0=unspecified/strict, 1=strict, 2=validated, 3=flexible + self.schema_mode: int = getattr(proto_label, "schema_mode", 0) def __repr__(self) -> str: - return f"LabelInfo(name={self.name!r}, version={self.version}, properties={len(self.properties)})" + return f"LabelInfo(name={self.name!r}, version={self.version}, properties={len(self.properties)}, schema_mode={self.schema_mode})" class EdgeTypeInfo: @@ -117,9 +119,10 @@ def __init__(self, proto_edge_type: Any) -> None: self.name: str = proto_edge_type.name self.version: int = proto_edge_type.version self.properties: list[PropertyDefinitionInfo] = [PropertyDefinitionInfo(p) for p in proto_edge_type.properties] + self.schema_mode: int = getattr(proto_edge_type, "schema_mode", 0) def __repr__(self) -> str: - return f"EdgeTypeInfo(name={self.name!r}, version={self.version}, properties={len(self.properties)})" + return f"EdgeTypeInfo(name={self.name!r}, version={self.version}, properties={len(self.properties)}, schema_mode={self.schema_mode})" class TraverseResult: @@ -365,6 +368,119 @@ async def get_edge_types(self) -> list[EdgeTypeInfo]: resp = await self._schema_stub.ListEdgeTypes(ListEdgeTypesRequest(), timeout=self._timeout) return [EdgeTypeInfo(et) for et in resp.edge_types] + async def create_label( + self, + name: str, + properties: list[dict[str, Any]] | None = None, + *, + schema_mode: str = "strict", + ) -> LabelInfo: + """Create a node label in the schema registry. + + Args: + name: Label name (e.g. ``"Person"``). + properties: Optional list of property dicts with keys + ``name`` (str), ``type`` (str), ``required`` (bool), + ``unique`` (bool). Type strings: ``"string"``, + ``"int64"``, ``"float64"``, ``"bool"``, ``"bytes"``, + ``"timestamp"``, ``"vector"``. + schema_mode: ``"strict"`` (default — reject undeclared props), + ``"validated"`` (allow extra props without interning), + ``"flexible"`` (no enforcement). + """ + from coordinode._proto.coordinode.v1.graph.schema_pb2 import ( # type: ignore[import] + CreateLabelRequest, + PropertyDefinition, + PropertyType, + SchemaMode, + ) + + _type_map = { + "int64": PropertyType.PROPERTY_TYPE_INT64, + "float64": PropertyType.PROPERTY_TYPE_FLOAT64, + "string": PropertyType.PROPERTY_TYPE_STRING, + "bool": PropertyType.PROPERTY_TYPE_BOOL, + "bytes": PropertyType.PROPERTY_TYPE_BYTES, + "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, + "vector": PropertyType.PROPERTY_TYPE_VECTOR, + "list": PropertyType.PROPERTY_TYPE_LIST, + "map": PropertyType.PROPERTY_TYPE_MAP, + } + _mode_map = { + "strict": SchemaMode.SCHEMA_MODE_STRICT, + "validated": SchemaMode.SCHEMA_MODE_VALIDATED, + "flexible": SchemaMode.SCHEMA_MODE_FLEXIBLE, + } + if schema_mode not in _mode_map: + raise ValueError(f"schema_mode must be one of {list(_mode_map)}, got {schema_mode!r}") + + proto_props = [] + for p in properties or []: + type_str = str(p.get("type", "string")).lower() + proto_props.append( + PropertyDefinition( + name=p["name"], + type=_type_map.get(type_str, PropertyType.PROPERTY_TYPE_STRING), + required=bool(p.get("required", False)), + unique=bool(p.get("unique", False)), + ) + ) + + req = CreateLabelRequest( + name=name, + properties=proto_props, + schema_mode=_mode_map[schema_mode], + ) + label = await self._schema_stub.CreateLabel(req, timeout=self._timeout) + return LabelInfo(label) + + async def create_edge_type( + self, + name: str, + properties: list[dict[str, Any]] | None = None, + ) -> EdgeTypeInfo: + """Create an edge type in the schema registry. + + Args: + name: Edge type name (e.g. ``"KNOWS"``). + properties: Optional list of property dicts with keys + ``name`` (str), ``type`` (str), ``required`` (bool), + ``unique`` (bool). Same type strings as :meth:`create_label`. + """ + from coordinode._proto.coordinode.v1.graph.schema_pb2 import ( # type: ignore[import] + CreateEdgeTypeRequest, + PropertyDefinition, + PropertyType, + ) + + _type_map = { + "int64": PropertyType.PROPERTY_TYPE_INT64, + "float64": PropertyType.PROPERTY_TYPE_FLOAT64, + "string": PropertyType.PROPERTY_TYPE_STRING, + "bool": PropertyType.PROPERTY_TYPE_BOOL, + "bytes": PropertyType.PROPERTY_TYPE_BYTES, + "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, + "vector": PropertyType.PROPERTY_TYPE_VECTOR, + "list": PropertyType.PROPERTY_TYPE_LIST, + "map": PropertyType.PROPERTY_TYPE_MAP, + } + + proto_props = [] + for p in properties or []: + type_str = str(p.get("type", "string")).lower() + proto_props.append( + PropertyDefinition( + name=p["name"], + type=_type_map.get(type_str, PropertyType.PROPERTY_TYPE_STRING), + required=bool(p.get("required", False)), + unique=bool(p.get("unique", False)), + ) + ) + + req = CreateEdgeTypeRequest(name=name, properties=proto_props) + et = await self._schema_stub.CreateEdgeType(req, timeout=self._timeout) + return EdgeTypeInfo(et) + async def traverse( self, start_node_id: int, @@ -544,6 +660,24 @@ def get_edge_types(self) -> list[EdgeTypeInfo]: """Return all edge types defined in the schema.""" return self._run(self._async.get_edge_types()) + def create_label( + self, + name: str, + properties: list[dict[str, Any]] | None = None, + *, + schema_mode: str = "strict", + ) -> LabelInfo: + """Create a node label in the schema registry.""" + return self._run(self._async.create_label(name, properties, schema_mode=schema_mode)) + + def create_edge_type( + self, + name: str, + properties: list[dict[str, Any]] | None = None, + ) -> EdgeTypeInfo: + """Create an edge type in the schema registry.""" + return self._run(self._async.create_edge_type(name, properties)) + def traverse( self, start_node_id: int, diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index cc0f2e7..54cc531 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -71,22 +71,13 @@ def refresh_schema(self) -> None: text = self._client.get_schema_text() self._schema = text structured = _parse_schema(text) - # Augment with relationship triples (start_label, type, end_label) via - # Cypher — get_schema_text() only lists edge types without direction. - # No LIMIT here intentionally: RETURN DISTINCT already collapses all edges - # to unique (src_label, rel_type, dst_label) combinations, so the result - # is bounded by the number of distinct relationship type triples, not by - # total edge count. Adding a LIMIT would silently drop relationship types - # that happen to appear beyond the limit, producing an incomplete schema. + # Augment with relationship triples (start_label, type, end_label). + # No LIMIT: RETURN DISTINCT bounds result by unique triples, not edge count. + # TODO: simplify to labels(a)[0] once subscript-on-function is in published image. rows = self._client.cypher( "MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels" ) if rows: - # Deduplicate after _first_label() normalization: RETURN DISTINCT operates on - # raw label lists, but _first_label(min()) can collapse different multi-label - # combinations to the same (start, type, end) triple (e.g. ['Employee','Person'] - # and ['Person','Employee'] both min-normalize to 'Employee'). Use a set to - # ensure each relationship triple appears at most once. triples: set[tuple[str, str, str]] = set() for row in rows: start = _first_label(row.get("src_labels")) @@ -276,9 +267,10 @@ def _stable_document_id(source: Any) -> str: def _first_label(labels: Any) -> str | None: """Extract a stable label from a labels() result (list of strings). - openCypher does not guarantee a stable ordering for labels(), so using - labels[0] would produce nondeterministic schema entries across calls. - We return the lexicographically smallest label as a deterministic rule. + CoordiNode nodes have exactly one label, so labels() always returns a + single-element list. min() gives a deterministic result for robustness. + TODO: replace with labels(n)[0] in Cypher once subscript-on-function + lands in the published Docker image. """ if isinstance(labels, list) and labels: return str(min(labels)) diff --git a/proto b/proto index 1b78ebd..381dbcd 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 1b78ebd35f9a41c88fddcc45addb7b91395f80f4 +Subproject commit 381dbcd72677112a393d485d8992eda1665d228c diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index fa14ce2..5eeb48e 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -426,6 +426,91 @@ async def test_async_create_node(): await c.cypher("MATCH (n:AsyncTest {tag: $tag}) DELETE n", params={"tag": tag}) +# ── create_label / create_edge_type ────────────────────────────────────────── + + +def test_create_label_returns_label_info(client): + """create_label() registers a label and returns LabelInfo.""" + name = f"CreateLabelTest{uid()}" + info = client.create_label( + name, + properties=[ + {"name": "title", "type": "string", "required": True}, + {"name": "score", "type": "float64"}, + ], + ) + assert isinstance(info, LabelInfo) + assert info.name == name + + +def test_create_label_appears_in_get_labels(client): + """Label created via create_label() appears in get_labels() once a node exists. + + Known limitation: ListLabels currently returns only labels that have at least + one node in the graph. Ideally it should also include schema-only labels + registered via create_label() (analogous to Neo4j returning schema-constrained + labels even without data). Tracked as a server-side gap. + """ + name = f"CreateLabelVisible{uid()}" + tag = uid() + client.create_label(name, properties=[{"name": "x", "type": "int64"}]) + # Workaround: create a node so the label appears in ListLabels. + client.cypher(f"CREATE (n:{name} {{x: 1, tag: $tag}})", params={"tag": tag}) + try: + labels = client.get_labels() + names = [lbl.name for lbl in labels] + assert name in names, f"{name} not in {names}" + finally: + client.cypher(f"MATCH (n:{name} {{tag: $tag}}) DELETE n", params={"tag": tag}) + + +def test_create_label_schema_mode_flexible(client): + """create_label() with schema_mode='flexible' is accepted by the server.""" + name = f"FlexLabel{uid()}" + info = client.create_label(name, schema_mode="flexible") + assert isinstance(info, LabelInfo) + assert info.name == name + + +def test_create_label_invalid_schema_mode_raises(client): + """create_label() with unknown schema_mode raises ValueError locally.""" + with pytest.raises(ValueError, match="schema_mode"): + client.create_label(f"Bad{uid()}", schema_mode="unknown") + + +def test_create_edge_type_returns_edge_type_info(client): + """create_edge_type() registers an edge type and returns EdgeTypeInfo.""" + name = f"CREATE_ET_{uid()}".upper() + info = client.create_edge_type( + name, + properties=[{"name": "since", "type": "timestamp"}], + ) + assert isinstance(info, EdgeTypeInfo) + assert info.name == name + + +def test_create_edge_type_appears_in_get_edge_types(client): + """Edge type created via create_edge_type() appears in get_edge_types() once an edge exists. + + Same known limitation as test_create_label_appears_in_get_labels: ListEdgeTypes + currently requires at least one edge of that type to exist in the graph. + """ + name = f"VISIBLE_ET_{uid()}".upper() + tag = uid() + client.create_edge_type(name) + # Workaround: create an edge so the type appears in ListEdgeTypes. + client.cypher( + f"CREATE (a:VisibleEtNode {{tag: $tag}})-[:{name}]->(b:VisibleEtNode {{tag: $tag}})", + params={"tag": tag}, + ) + try: + edge_types = client.get_edge_types() + names = [et.name for et in edge_types] + assert name in names, f"{name} not in {names}" + finally: + client.cypher("MATCH (n:VisibleEtNode {tag: $tag}) DETACH DELETE n", params={"tag": tag}) + + # ── Vector search ───────────────────────────────────────────────────────────── diff --git a/tests/unit/test_schema_crud.py b/tests/unit/test_schema_crud.py index 3e6495c..398a7e7 100644 --- a/tests/unit/test_schema_crud.py +++ b/tests/unit/test_schema_crud.py @@ -35,19 +35,21 @@ def __init__(self, name: str, type_: int, required: bool = False, unique: bool = class _FakeLabel: """Matches proto Label shape.""" - def __init__(self, name: str, version: int = 1, properties=None) -> None: + def __init__(self, name: str, version: int = 1, properties=None, schema_mode: int = 0) -> None: self.name = name self.version = version self.properties = properties or [] + self.schema_mode = schema_mode class _FakeEdgeType: """Matches proto EdgeType shape.""" - def __init__(self, name: str, version: int = 1, properties=None) -> None: + def __init__(self, name: str, version: int = 1, properties=None, schema_mode: int = 0) -> None: self.name = name self.version = version self.properties = properties or [] + self.schema_mode = schema_mode class _FakeNode: @@ -127,6 +129,36 @@ def test_version_zero(self): label = LabelInfo(_FakeLabel("Draft", version=0)) assert label.version == 0 + def test_schema_mode_defaults_to_zero(self): + label = LabelInfo(_FakeLabel("Person")) + assert label.schema_mode == 0 + + def test_schema_mode_strict(self): + label = LabelInfo(_FakeLabel("Person", schema_mode=1)) + assert label.schema_mode == 1 + + def test_schema_mode_validated(self): + label = LabelInfo(_FakeLabel("Person", schema_mode=2)) + assert label.schema_mode == 2 + + def test_schema_mode_flexible(self): + label = LabelInfo(_FakeLabel("Person", schema_mode=3)) + assert label.schema_mode == 3 + + def test_schema_mode_in_repr(self): + label = LabelInfo(_FakeLabel("Person", schema_mode=1)) + assert "schema_mode" in repr(label) + + def test_schema_mode_missing_from_proto_defaults_zero(self): + # Proto objects without schema_mode attribute (older server) → 0. + class _OldLabel: + name = "Legacy" + version = 1 + properties = [] + + label = LabelInfo(_OldLabel()) + assert label.schema_mode == 0 + # ── EdgeTypeInfo ───────────────────────────────────────────────────────────── @@ -150,6 +182,23 @@ def test_repr_contains_name(self): et = EdgeTypeInfo(_FakeEdgeType("RATED")) assert "RATED" in repr(et) + def test_schema_mode_defaults_to_zero(self): + et = EdgeTypeInfo(_FakeEdgeType("KNOWS")) + assert et.schema_mode == 0 + + def test_schema_mode_propagated(self): + et = EdgeTypeInfo(_FakeEdgeType("KNOWS", schema_mode=2)) + assert et.schema_mode == 2 + + def test_schema_mode_missing_from_proto_defaults_zero(self): + class _OldEdgeType: + name = "LEGACY" + version = 1 + properties = [] + + et = EdgeTypeInfo(_OldEdgeType()) + assert et.schema_mode == 0 + # ── TraverseResult ─────────────────────────────────────────────────────────── diff --git a/uv.lock b/uv.lock index 50ea668..72a32e3 100644 --- a/uv.lock +++ b/uv.lock @@ -359,7 +359,7 @@ provides-extras = ["dev"] [[package]] name = "coordinode-workspace" -version = "0.4.4" +version = "0.6.0" source = { virtual = "." } dependencies = [ { name = "googleapis-common-protos" }, From 9122f610db3c907d00e7b95e4d62847a09176aad Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 18:18:53 +0300 Subject: [PATCH 02/86] feat(demo): add Google Colab notebooks + client= param for embedded support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add client= parameter to CoordinodeGraph.__init__ and CoordinodePropertyGraphStore.__init__: accepts any object with .cypher() instead of creating a gRPC CoordinodeClient from addr. Enables passing LocalClient from coordinode-embedded. - Rewrite all 4 demo notebooks for Google Colab: * 00_seed_data.ipynb — seeds tech-industry knowledge graph; Colab uses LocalClient(":memory:") directly (same .cypher() API) * 01_llama_index_property_graph.ipynb — PropertyGraphStore via _EmbeddedAdapter wrapping LocalClient; get_schema_text() via Cypher fallback * 02_langchain_graph_chain.ipynb — CoordinodeGraph with client= adapter; OpenAI GraphCypherQAChain section is optional (auto-skips without key) * 03_langgraph_agent.ipynb — LocalClient direct; LangGraph agent with mock demo first (no LLM key needed), LLM section optional - Each notebook: IN_COLAB detection, installs coordinode-embedded from GitHub source (Rust build, ~5 min first run), Open in Colab badge in first cell - Add demo/README.md with Colab badge table for all 4 notebooks Colab links: https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/01_llama_index_property_graph.ipynb https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/02_langchain_graph_chain.ipynb https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/03_langgraph_agent.ipynb --- demo/README.md | 38 ++ demo/notebooks/00_seed_data.ipynb | 393 ++++++++++++++++++ .../01_llama_index_property_graph.ipynb | 384 +++++++++++++++++ demo/notebooks/02_langchain_graph_chain.ipynb | 382 +++++++++++++++++ demo/notebooks/03_langgraph_agent.ipynb | 375 +++++++++++++++++ .../langchain_coordinode/graph.py | 7 +- .../graph_stores/coordinode/base.py | 5 +- 7 files changed, 1582 insertions(+), 2 deletions(-) create mode 100644 demo/README.md create mode 100644 demo/notebooks/00_seed_data.ipynb create mode 100644 demo/notebooks/01_llama_index_property_graph.ipynb create mode 100644 demo/notebooks/02_langchain_graph_chain.ipynb create mode 100644 demo/notebooks/03_langgraph_agent.ipynb diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..1aa7af5 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,38 @@ +# CoordiNode Demo Notebooks + +Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. + +## Open in Google Colab (no setup required) + +| Notebook | What it shows | +|----------|---------------| +| [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb) **Seed Data** | Build a tech-industry knowledge graph (~35 relationships) | +| [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/01_llama_index_property_graph.ipynb) **LlamaIndex** | `CoordinodePropertyGraphStore`: upsert, triplets, structured query | +| [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/02_langchain_graph_chain.ipynb) **LangChain** | `CoordinodeGraph`: add_graph_documents, schema, GraphCypherQAChain | +| [![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/03_langgraph_agent.ipynb) **LangGraph** | Agent with CoordiNode as graph memory — save/query/traverse | + +> **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). +> Subsequent runs use Colab's pip cache. + +## Run locally (Docker Compose) + +```bash +cd demo/ +docker compose up -d --build +``` + +Open: http://localhost:38888 (token: `demo`) + +| Port | Service | +|------|---------| +| 37080 | CoordiNode gRPC | +| 38888 | Jupyter Lab | + +## With OpenAI (optional) + +Notebooks 02 and 03 have optional sections that use `OPENAI_API_KEY`. +They auto-skip when the key is absent — all core features work without LLM. + +```bash +OPENAI_API_KEY=sk-... docker compose up -d +``` diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb new file mode 100644 index 0000000..b91c88f --- /dev/null +++ b/demo/notebooks/00_seed_data.ipynb @@ -0,0 +1,393 @@ +{ + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000001", + "metadata": {}, + "source": [ + "# Seed Demo Data\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb)\n", + "\n", + "Populates CoordiNode with a **tech industry knowledge graph** you can explore\n", + "in notebooks 01–03.\n", + "\n", + "**Graph contents:**\n", + "- 10 people (engineers, researchers, founders)\n", + "- 6 companies\n", + "- 8 technologies / research areas\n", + "- ~35 relationships (WORKS_AT, FOUNDED, KNOWS, RESEARCHES, INVENTED, ACQUIRED, USES, …)\n", + "\n", + "All nodes carry a `demo=true` property so they can be bulk-deleted without\n", + "touching other data.\n", + "\n", + "**Environments:**\n", + "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", + "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC." + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000002", + "metadata": {}, + "source": [ + "## Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000003", + "metadata": {}, + "outputs": [], + "source": [ + "import sys, subprocess\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "\n", + "if IN_COLAB:\n", + " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run(\n", + " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", + " shell=True, check=True\n", + " )\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "else:\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'coordinode',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print('Ready')" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Connect to CoordiNode\n", + "\n", + "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", + "- **Local**: checks for a running server on port 7080, or starts one via Docker." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "import os, socket, subprocess, time\n", + "\n", + "if IN_COLAB:\n", + " from coordinode_embedded import LocalClient\n", + " client = LocalClient(\":memory:\")\n", + " print('Using embedded LocalClient (in-process)')\n", + "else:\n", + " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", + " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + "\n", + " def _port_open(port):\n", + " try:\n", + " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", + " except OSError: return False\n", + "\n", + " if os.environ.get('COORDINODE_ADDR'):\n", + " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", + " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " elif _port_open(GRPC_PORT):\n", + " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " else:\n", + " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", + " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", + " capture_output=True, text=True)\n", + " if proc.returncode != 0:\n", + " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " container_id = proc.stdout.strip()\n", + " for _ in range(30):\n", + " if _port_open(GRPC_PORT): break\n", + " time.sleep(1)\n", + " else:\n", + " subprocess.run(['docker', 'stop', container_id])\n", + " raise RuntimeError('CoordiNode did not start in 30 s')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + "\n", + " from coordinode import CoordinodeClient\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f'Connected to {COORDINODE_ADDR}')" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## Step 1 — Clear previous demo data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "result = client.cypher('MATCH (n {demo: true}) DETACH DELETE n')\n", + "print('Previous demo data removed')" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## Step 2 — Create nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "# ── People ────────────────────────────────────────────────────────────────\n", + "people = [\n", + " {'name': 'Alice Chen', 'role': 'ML Researcher', 'org': 'DeepMind', 'field': 'Reinforcement Learning'},\n", + " {'name': 'Bob Torres', 'role': 'Staff Engineer', 'org': 'Google', 'field': 'Distributed Systems'},\n", + " {'name': 'Carol Smith', 'role': 'Founder & CEO', 'org': 'Synthex', 'field': 'NLP'},\n", + " {'name': 'David Park', 'role': 'Research Scientist', 'org': 'OpenAI', 'field': 'LLMs'},\n", + " {'name': 'Eva Müller', 'role': 'Systems Architect', 'org': 'Synthex', 'field': 'Graph Databases'},\n", + " {'name': 'Frank Liu', 'role': 'Principal Engineer', 'org': 'Meta', 'field': 'Graph ML'},\n", + " {'name': 'Grace Okafor', 'role': 'PhD Researcher', 'org': 'MIT', 'field': 'Knowledge Graphs'},\n", + " {'name': 'Henry Rossi', 'role': 'CTO', 'org': 'Synthex', 'field': 'Databases'},\n", + " {'name': 'Isla Nakamura', 'role': 'Senior Researcher', 'org': 'DeepMind', 'field': 'Graph Neural Networks'},\n", + " {'name': 'James Wright', 'role': 'Engineering Lead', 'org': 'Google', 'field': 'Search'},\n", + "]\n", + "\n", + "for p in people:\n", + " client.cypher(\n", + " 'MERGE (n:Person {name: $name}) '\n", + " 'SET n.role = $role, n.field = $field, n.demo = true',\n", + " params=p\n", + " )\n", + "\n", + "print(f'Created {len(people)} people')\n", + "\n", + "# ── Companies ─────────────────────────────────────────────────────────────\n", + "companies = [\n", + " {'name': 'Google', 'industry': 'Technology', 'founded': 1998, 'hq': 'Mountain View'},\n", + " {'name': 'Meta', 'industry': 'Technology', 'founded': 2004, 'hq': 'Menlo Park'},\n", + " {'name': 'OpenAI', 'industry': 'AI Research', 'founded': 2015, 'hq': 'San Francisco'},\n", + " {'name': 'DeepMind', 'industry': 'AI Research', 'founded': 2010, 'hq': 'London'},\n", + " {'name': 'Synthex', 'industry': 'AI Startup', 'founded': 2021, 'hq': 'Berlin'},\n", + " {'name': 'MIT', 'industry': 'Academia', 'founded': 1861, 'hq': 'Cambridge'},\n", + "]\n", + "\n", + "for c in companies:\n", + " client.cypher(\n", + " 'MERGE (n:Company {name: $name}) '\n", + " 'SET n.industry = $industry, n.founded = $founded, n.hq = $hq, n.demo = true',\n", + " params=c\n", + " )\n", + "\n", + "print(f'Created {len(companies)} companies')\n", + "\n", + "# ── Technologies ──────────────────────────────────────────────────────────\n", + "technologies = [\n", + " {'name': 'Transformer', 'type': 'Architecture', 'year': 2017},\n", + " {'name': 'Graph Neural Network', 'type': 'Algorithm', 'year': 2009},\n", + " {'name': 'Reinforcement Learning','type': 'Paradigm', 'year': 1980},\n", + " {'name': 'Knowledge Graph', 'type': 'Data Model', 'year': 2012},\n", + " {'name': 'Vector Database', 'type': 'Infrastructure','year': 2019},\n", + " {'name': 'RAG', 'type': 'Technique', 'year': 2020},\n", + " {'name': 'LLM', 'type': 'Model Class', 'year': 2018},\n", + " {'name': 'GraphRAG', 'type': 'Technique', 'year': 2023},\n", + "]\n", + "\n", + "for t in technologies:\n", + " client.cypher(\n", + " 'MERGE (n:Technology {name: $name}) '\n", + " 'SET n.type = $type, n.year = $year, n.demo = true',\n", + " params=t\n", + " )\n", + "\n", + "print(f'Created {len(technologies)} technologies')" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## Step 3 — Create relationships" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "edges = [\n", + " # WORKS_AT\n", + " ('Alice Chen', 'WORKS_AT', 'DeepMind', {}),\n", + " ('Bob Torres', 'WORKS_AT', 'Google', {}),\n", + " ('Carol Smith', 'WORKS_AT', 'Synthex', {'since': 2021}),\n", + " ('David Park', 'WORKS_AT', 'OpenAI', {}),\n", + " ('Eva Müller', 'WORKS_AT', 'Synthex', {'since': 2022}),\n", + " ('Frank Liu', 'WORKS_AT', 'Meta', {}),\n", + " ('Grace Okafor', 'WORKS_AT', 'MIT', {}),\n", + " ('Henry Rossi', 'WORKS_AT', 'Synthex', {'since': 2021}),\n", + " ('Isla Nakamura', 'WORKS_AT', 'DeepMind', {}),\n", + " ('James Wright', 'WORKS_AT', 'Google', {}),\n", + " # FOUNDED\n", + " ('Carol Smith', 'FOUNDED', 'Synthex', {'year': 2021}),\n", + " ('Henry Rossi', 'CO_FOUNDED', 'Synthex', {'year': 2021}),\n", + " # KNOWS\n", + " ('Alice Chen', 'KNOWS', 'Isla Nakamura', {}),\n", + " ('Alice Chen', 'KNOWS', 'David Park', {}),\n", + " ('Carol Smith', 'KNOWS', 'Bob Torres', {}),\n", + " ('Grace Okafor', 'KNOWS', 'Alice Chen', {}),\n", + " ('Frank Liu', 'KNOWS', 'James Wright', {}),\n", + " ('Eva Müller', 'KNOWS', 'Grace Okafor', {}),\n", + " # RESEARCHES / WORKS_ON\n", + " ('Alice Chen', 'RESEARCHES', 'Reinforcement Learning', {'since': 2019}),\n", + " ('David Park', 'RESEARCHES', 'LLM', {'since': 2020}),\n", + " ('Grace Okafor', 'RESEARCHES', 'Knowledge Graph', {'since': 2021}),\n", + " ('Isla Nakamura', 'RESEARCHES', 'Graph Neural Network', {'since': 2020}),\n", + " ('Frank Liu', 'RESEARCHES', 'Graph Neural Network', {}),\n", + " ('Grace Okafor', 'RESEARCHES', 'GraphRAG', {'since': 2023}),\n", + " # USES\n", + " ('Synthex', 'USES', 'Knowledge Graph', {}),\n", + " ('Synthex', 'USES', 'Vector Database', {}),\n", + " ('Synthex', 'USES', 'RAG', {}),\n", + " ('OpenAI', 'USES', 'Transformer', {}),\n", + " ('Google', 'USES', 'Transformer', {}),\n", + " # ACQUIRED\n", + " ('Google', 'ACQUIRED', 'DeepMind', {'year': 2014}),\n", + " # BUILDS_ON\n", + " ('GraphRAG', 'BUILDS_ON', 'Knowledge Graph', {}),\n", + " ('GraphRAG', 'BUILDS_ON', 'RAG', {}),\n", + " ('RAG', 'BUILDS_ON', 'Vector Database', {}),\n", + " ('LLM', 'BUILDS_ON', 'Transformer', {}),\n", + "]\n", + "\n", + "src_names = {p['name'] for p in people}\n", + "tech_names = {t['name'] for t in technologies}\n", + "company_names = {c['name'] for c in companies}\n", + "\n", + "def _label(name):\n", + " if name in src_names: return 'Person'\n", + " if name in tech_names: return 'Technology'\n", + " return 'Company'\n", + "\n", + "for src, rel, dst, props in edges:\n", + " src_label = _label(src)\n", + " dst_label = _label(dst)\n", + " set_clause = ', '.join(f'r.{k} = ${k}' for k in props) if props else ''\n", + " set_part = f' SET {set_clause}' if set_clause else ''\n", + " client.cypher(\n", + " f'MATCH (a:{src_label} {{name: $src}}) '\n", + " f'MATCH (b:{dst_label} {{name: $dst}}) '\n", + " f'MERGE (a)-[r:{rel}]->(b)' + set_part,\n", + " params={'src': src, 'dst': dst, **props}\n", + " )\n", + "\n", + "print(f'Created {len(edges)} relationships')" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000012", + "metadata": {}, + "source": [ + "## Step 4 — Verify" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000013", + "metadata": {}, + "outputs": [], + "source": [ + "from collections import Counter\n", + "\n", + "print('Node counts:')\n", + "for label in ['Person', 'Company', 'Technology']:\n", + " rows = client.cypher(\n", + " f'MATCH (n:{label} {{demo: true}}) RETURN count(n) AS count'\n", + " )\n", + " print(f\" {label:15s} {rows[0]['count']}\")\n", + "\n", + "# Fetch all types and count in Python (avoids aggregation limitations)\n", + "rels = client.cypher('MATCH (a {demo: true})-[r]->(b) RETURN type(r) AS rel')\n", + "counts = Counter(r['rel'] for r in rels)\n", + "print('\\nRelationship counts:')\n", + "for rel, cnt in sorted(counts.items(), key=lambda x: -x[1]):\n", + " print(f' {rel:20s} {cnt}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000014", + "metadata": {}, + "outputs": [], + "source": [ + "print('=== Who works at Synthex? ===')\n", + "rows = client.cypher(\n", + " 'MATCH (p:Person)-[:WORKS_AT]->(c:Company {name: $co}) RETURN p.name AS name, p.role AS role',\n", + " params={'co': 'Synthex'}\n", + ")\n", + "for r in rows:\n", + " print(f\" {r['name']} — {r['role']}\")\n", + "\n", + "print('\\n=== What does Synthex use? ===')\n", + "rows = client.cypher(\n", + " 'MATCH (c:Company {name: $co})-[:USES]->(t:Technology) RETURN t.name AS name',\n", + " params={'co': 'Synthex'}\n", + ")\n", + "for r in rows:\n", + " print(f\" {r['name']}\")\n", + "\n", + "print('\\n=== GraphRAG dependency chain ===')\n", + "rows = client.cypher(\n", + " 'MATCH (t:Technology {name: $tech})-[:BUILDS_ON*1..3]->(dep) RETURN dep.name AS dependency',\n", + " params={'tech': 'GraphRAG'}\n", + ")\n", + "for r in rows:\n", + " print(f\" → {r['dependency']}\")\n", + "\n", + "print('\\n✓ Demo data ready — open notebooks 01, 02, 03 to explore!')\n", + "client.close()" + ] + } + ] +} diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb new file mode 100644 index 0000000..a653a42 --- /dev/null +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -0,0 +1,384 @@ +{ + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000001", + "metadata": {}, + "source": [ + "# LlamaIndex + CoordiNode: PropertyGraphIndex\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/01_llama_index_property_graph.ipynb)\n", + "\n", + "Demonstrates `CoordinodePropertyGraphStore` as a backend for LlamaIndex `PropertyGraphIndex`.\n", + "\n", + "**What works right now:**\n", + "- `upsert_nodes` / `upsert_relations` — idempotent MERGE (safe to call multiple times)\n", + "- `get()` — look up nodes by ID or properties\n", + "- `get_triplets()` — all edges (wildcard) or filtered by relation type / entity name\n", + "- `get_rel_map()` — outgoing relations for a set of nodes (depth=1)\n", + "- `structured_query()` — arbitrary Cypher pass-through\n", + "- `delete()` — remove nodes by id or name\n", + "- `get_schema()` — live text schema of the graph\n", + "\n", + "**Environments:**\n", + "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", + "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC." + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000002", + "metadata": {}, + "source": [ + "## Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000003", + "metadata": {}, + "outputs": [], + "source": [ + "import sys, subprocess\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "\n", + "if IN_COLAB:\n", + " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run(\n", + " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", + " shell=True, check=True\n", + " )\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", + " 'llama-index-graph-stores-coordinode',\n", + " 'llama-index-core',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "else:\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'coordinode',\n", + " 'llama-index-graph-stores-coordinode',\n", + " 'llama-index-core',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print('SDK installed')" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Adapter for embedded mode\n", + "\n", + "In Colab we use `LocalClient` (embedded engine) which has the same `.cypher()` API as\n", + "`CoordinodeClient`. The `_EmbeddedAdapter` below adds the extra methods that\n", + "`CoordinodePropertyGraphStore` expects when it receives a pre-built `client=` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "class _EmbeddedAdapter:\n", + " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + " def __init__(self, local_client):\n", + " self._lc = local_client\n", + "\n", + " def cypher(self, query, params=None):\n", + " return self._lc.cypher(query, params or {})\n", + "\n", + " def get_schema_text(self):\n", + " lbls = self._lc.cypher(\n", + " 'MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl'\n", + " )\n", + " rels = self._lc.cypher(\n", + " 'MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t'\n", + " )\n", + " lines = ['Node labels:']\n", + " for r in lbls: lines.append(f\" - {r['lbl']}\")\n", + " lines.append('\\nEdge types:')\n", + " for r in rels: lines.append(f\" - {r['t']}\")\n", + " return '\\n'.join(lines)\n", + "\n", + " def vector_search(self, **kwargs): return []\n", + " def close(self): pass\n", + " def get_labels(self): return []\n", + " def get_edge_types(self): return []" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## Connect to CoordiNode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "import os, socket, subprocess, time\n", + "\n", + "if IN_COLAB:\n", + " from coordinode_embedded import LocalClient\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print('Using embedded LocalClient (in-process)')\n", + "else:\n", + " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", + " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + "\n", + " def _port_open(port):\n", + " try:\n", + " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", + " except OSError: return False\n", + "\n", + " if os.environ.get('COORDINODE_ADDR'):\n", + " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", + " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " elif _port_open(GRPC_PORT):\n", + " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " else:\n", + " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", + " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", + " capture_output=True, text=True)\n", + " if proc.returncode != 0:\n", + " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " container_id = proc.stdout.strip()\n", + " for _ in range(30):\n", + " if _port_open(GRPC_PORT): break\n", + " time.sleep(1)\n", + " else:\n", + " subprocess.run(['docker', 'stop', container_id])\n", + " raise RuntimeError('CoordiNode did not start in 30 s')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + "\n", + " from coordinode import CoordinodeClient\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f'Connected to {COORDINODE_ADDR}')" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## Create the property graph store\n", + "\n", + "Pass the pre-built `client=` object so the store doesn't open a second connection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "from llama_index.graph_stores.coordinode import CoordinodePropertyGraphStore\n", + "from llama_index.core.graph_stores.types import EntityNode, Relation\n", + "\n", + "store = CoordinodePropertyGraphStore(client=client)\n", + "print('Connected. Schema:')\n", + "print(store.get_schema()[:300])" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## 1. Upsert nodes and relations\n", + "\n", + "Each node gets a unique tag so this notebook can run multiple times without conflicts." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "import uuid\n", + "tag = uuid.uuid4().hex[:6]\n", + "\n", + "nodes = [\n", + " EntityNode(label='Person', name=f'Alice-{tag}', properties={'role': 'researcher', 'field': 'AI'}),\n", + " EntityNode(label='Person', name=f'Bob-{tag}', properties={'role': 'engineer', 'field': 'ML'}),\n", + " EntityNode(label='Topic', name=f'GraphRAG-{tag}', properties={'domain': 'knowledge graphs'}),\n", + "]\n", + "store.upsert_nodes(nodes)\n", + "print('Upserted nodes:', [n.name for n in nodes])\n", + "\n", + "alice, bob, graphrag = nodes\n", + "relations = [\n", + " Relation(label='RESEARCHES', source_id=alice.id, target_id=graphrag.id, properties={'since': 2023}),\n", + " Relation(label='COLLABORATES', source_id=alice.id, target_id=bob.id),\n", + " Relation(label='IMPLEMENTS', source_id=bob.id, target_id=graphrag.id),\n", + "]\n", + "store.upsert_relations(relations)\n", + "print('Upserted relations:', [r.label for r in relations])" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000012", + "metadata": {}, + "source": [ + "## 2. get_triplets — all edges from a node (wildcard)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000013", + "metadata": {}, + "outputs": [], + "source": [ + "triplets = store.get_triplets(entity_names=[f'Alice-{tag}'])\n", + "print(f'Triplets for Alice-{tag}:')\n", + "for src, rel, dst in triplets:\n", + " print(f' {src.name} --[{rel.label}]--> {dst.name}')" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000014", + "metadata": {}, + "source": [ + "## 3. get_rel_map — relations for a set of nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000015", + "metadata": {}, + "outputs": [], + "source": [ + "found_alice = store.get(properties={'name': f'Alice-{tag}'})\n", + "rel_map = store.get_rel_map(found_alice, depth=1, limit=20)\n", + "print(f'Rel map for Alice-{tag} ({len(rel_map)} rows):')\n", + "for src, rel, dst in rel_map:\n", + " print(f' {src.name} --[{rel.label}]--> {dst.name}')" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000016", + "metadata": {}, + "source": [ + "## 4. structured_query — arbitrary Cypher" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000017", + "metadata": {}, + "outputs": [], + "source": [ + "rows = store.structured_query(\n", + " 'MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)'\n", + " ' WHERE p.name STARTS WITH $prefix'\n", + " ' RETURN p.name AS person, t.name AS topic, r.since AS since',\n", + " param_map={'prefix': f'Alice-{tag[:4]}'}\n", + ")\n", + "print('Query result:', rows)" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000018", + "metadata": {}, + "source": [ + "## 5. get_schema" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000019", + "metadata": {}, + "outputs": [], + "source": [ + "schema = store.get_schema()\n", + "print(schema)" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000020", + "metadata": {}, + "source": [ + "## 6. Idempotency — double upsert must not duplicate edges" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000021", + "metadata": {}, + "outputs": [], + "source": [ + "store.upsert_relations(relations) # second call — should still be exactly 1 edge\n", + "rows = store.structured_query(\n", + " 'MATCH (a {name: $src})-[r:RESEARCHES]->(b {name: $dst}) RETURN count(r) AS cnt',\n", + " param_map={'src': f'Alice-{tag}', 'dst': f'GraphRAG-{tag}'}\n", + ")\n", + "print('Edge count after double upsert (expect 1):', rows[0]['cnt'])" + ] + }, + { + "cell_type": "markdown", + "id": "b2c3d4e5-0001-0000-0000-000000000022", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b2c3d4e5-0001-0000-0000-000000000023", + "metadata": {}, + "outputs": [], + "source": [ + "store.delete(entity_names=[f'Alice-{tag}', f'Bob-{tag}', f'GraphRAG-{tag}'])\n", + "print('Cleaned up')\n", + "store.close()" + ] + } + ] +} diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb new file mode 100644 index 0000000..6f6154e --- /dev/null +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -0,0 +1,382 @@ +{ + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000001", + "metadata": {}, + "source": [ + "# LangChain + CoordiNode: Graph Chain\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/02_langchain_graph_chain.ipynb)\n", + "\n", + "Demonstrates `CoordinodeGraph` as a Knowledge Graph backend for LangChain.\n", + "\n", + "**What works right now:**\n", + "- `graph.query()` — arbitrary Cypher pass-through\n", + "- `graph.schema` / `refresh_schema()` — live graph schema\n", + "- `add_graph_documents()` — add Nodes + Relationships from a LangChain `GraphDocument`\n", + "- `GraphCypherQAChain` — LLM generates Cypher from a natural-language question *(requires `OPENAI_API_KEY`)*\n", + "\n", + "**Environments:**\n", + "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", + "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC." + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000002", + "metadata": {}, + "source": [ + "## Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000003", + "metadata": {}, + "outputs": [], + "source": [ + "import sys, subprocess\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "\n", + "if IN_COLAB:\n", + " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run(\n", + " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", + " shell=True, check=True\n", + " )\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", + " 'langchain-coordinode',\n", + " 'langchain',\n", + " 'langchain-openai',\n", + " 'langchain-community',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "else:\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'coordinode',\n", + " 'langchain-coordinode',\n", + " 'langchain',\n", + " 'langchain-openai',\n", + " 'langchain-community',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print('SDK installed')" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Adapter for embedded mode\n", + "\n", + "In Colab we use `LocalClient` (embedded engine) which has the same `.cypher()` API as\n", + "`CoordinodeClient`. The `_EmbeddedAdapter` below adds the extra methods that\n", + "`CoordinodeGraph` expects when it receives a pre-built `client=` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "class _EmbeddedAdapter:\n", + " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + " def __init__(self, local_client):\n", + " self._lc = local_client\n", + "\n", + " def cypher(self, query, params=None):\n", + " return self._lc.cypher(query, params or {})\n", + "\n", + " def get_schema_text(self):\n", + " lbls = self._lc.cypher(\n", + " 'MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl'\n", + " )\n", + " rels = self._lc.cypher(\n", + " 'MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t'\n", + " )\n", + " lines = ['Node labels:']\n", + " for r in lbls: lines.append(f\" - {r['lbl']}\")\n", + " lines.append('\\nEdge types:')\n", + " for r in rels: lines.append(f\" - {r['t']}\")\n", + " return '\\n'.join(lines)\n", + "\n", + " def vector_search(self, **kwargs): return []\n", + " def close(self): pass\n", + " def get_labels(self): return []\n", + " def get_edge_types(self): return []" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## Connect to CoordiNode" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "import os, socket, subprocess, time\n", + "\n", + "if IN_COLAB:\n", + " from coordinode_embedded import LocalClient\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print('Using embedded LocalClient (in-process)')\n", + "else:\n", + " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", + " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + "\n", + " def _port_open(port):\n", + " try:\n", + " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", + " except OSError: return False\n", + "\n", + " if os.environ.get('COORDINODE_ADDR'):\n", + " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", + " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " elif _port_open(GRPC_PORT):\n", + " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " else:\n", + " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", + " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", + " capture_output=True, text=True)\n", + " if proc.returncode != 0:\n", + " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " container_id = proc.stdout.strip()\n", + " for _ in range(30):\n", + " if _port_open(GRPC_PORT): break\n", + " time.sleep(1)\n", + " else:\n", + " subprocess.run(['docker', 'stop', container_id])\n", + " raise RuntimeError('CoordiNode did not start in 30 s')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + "\n", + " from coordinode import CoordinodeClient\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f'Connected to {COORDINODE_ADDR}')" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## Create the graph store\n", + "\n", + "Pass the pre-built `client=` object so the store doesn't open a second connection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "import os, uuid\n", + "from langchain_coordinode import CoordinodeGraph\n", + "from langchain_community.graphs.graph_document import GraphDocument, Node, Relationship\n", + "from langchain_core.documents import Document\n", + "\n", + "graph = CoordinodeGraph(client=client)\n", + "print('Connected. Schema preview:')\n", + "print(graph.schema[:300])" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## 1. add_graph_documents\n", + "\n", + "LangChain `GraphDocument` wraps nodes and relationships from an LLM extraction pipeline.\n", + "Here we build one manually to show the insert path." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "tag = uuid.uuid4().hex[:6]\n", + "\n", + "nodes = [\n", + " Node(id=f'Turing-{tag}', type='Scientist', properties={'born': 1912, 'field': 'computer science'}),\n", + " Node(id=f'Shannon-{tag}', type='Scientist', properties={'born': 1916, 'field': 'information theory'}),\n", + " Node(id=f'Cryptography-{tag}', type='Field', properties={'era': 'modern'}),\n", + "]\n", + "rels = [\n", + " Relationship(source=nodes[0], target=nodes[2], type='FOUNDED', properties={'year': 1936}),\n", + " Relationship(source=nodes[1], target=nodes[2], type='CONTRIBUTED_TO'),\n", + " Relationship(source=nodes[0], target=nodes[1], type='INFLUENCED'),\n", + "]\n", + "doc = GraphDocument(nodes=nodes, relationships=rels, source=Document(page_content='Turing and Shannon'))\n", + "graph.add_graph_documents([doc])\n", + "print('Documents added')" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000012", + "metadata": {}, + "source": [ + "## 2. query — direct Cypher" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000013", + "metadata": {}, + "outputs": [], + "source": [ + "rows = graph.query(\n", + " 'MATCH (s:Scientist)-[r]->(f:Field)'\n", + " ' WHERE s.name STARTS WITH $prefix'\n", + " ' RETURN s.name AS scientist, type(r) AS relation, f.name AS field',\n", + " params={'prefix': f'Turing-{tag[:4]}'}\n", + ")\n", + "print('Scientists → Fields:')\n", + "for r in rows:\n", + " print(f\" {r['scientist']} --[{r['relation']}]--> {r['field']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000014", + "metadata": {}, + "source": [ + "## 3. refresh_schema — structured_schema dict" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000015", + "metadata": {}, + "outputs": [], + "source": [ + "graph.refresh_schema()\n", + "print('node_props keys:', list(graph.structured_schema.get('node_props', {}).keys())[:10])\n", + "print('relationships:', graph.structured_schema.get('relationships', [])[:5])" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000016", + "metadata": {}, + "source": [ + "## 4. Idempotency check\n", + "\n", + "`add_graph_documents` uses MERGE internally — adding the same document twice must not\n", + "create duplicate edges." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000017", + "metadata": {}, + "outputs": [], + "source": [ + "graph.add_graph_documents([doc]) # second upsert — must not create a duplicate edge\n", + "cnt = graph.query(\n", + " 'MATCH (a {name: $src})-[r:FOUNDED]->(b {name: $dst}) RETURN count(r) AS cnt',\n", + " params={'src': f'Turing-{tag}', 'dst': f'Cryptography-{tag}'}\n", + ")\n", + "print('FOUNDED edge count after double upsert (expect 1):', cnt[0]['cnt'])" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000018", + "metadata": {}, + "source": [ + "## 5. GraphCypherQAChain — LLM-powered Cypher (optional)\n", + "\n", + "> **This section requires `OPENAI_API_KEY`.** Set it in your environment or via\n", + "> `os.environ['OPENAI_API_KEY'] = 'sk-...'` before running.\n", + "> The cell is skipped automatically when the key is absent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000019", + "metadata": {}, + "outputs": [], + "source": [ + "if not os.environ.get('OPENAI_API_KEY'):\n", + " print('Skipping: OPENAI_API_KEY is not set.'\n", + " ' Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.')\n", + "else:\n", + " from langchain.chains import GraphCypherQAChain\n", + " from langchain_openai import ChatOpenAI\n", + "\n", + " chain = GraphCypherQAChain.from_llm(\n", + " ChatOpenAI(model='gpt-4o-mini', temperature=0),\n", + " graph=graph,\n", + " verbose=True,\n", + " )\n", + " result = chain.invoke('Who influenced Shannon?')\n", + " print('Answer:', result['result'])" + ] + }, + { + "cell_type": "markdown", + "id": "c3d4e5f6-0002-0000-0000-000000000020", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c3d4e5f6-0002-0000-0000-000000000021", + "metadata": {}, + "outputs": [], + "source": [ + "graph.query('MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n', params={'tag': tag})\n", + "print('Cleaned up')\n", + "graph.close()" + ] + } + ] +} diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb new file mode 100644 index 0000000..7e01614 --- /dev/null +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -0,0 +1,375 @@ +{ + "nbformat": 4, + "nbformat_minor": 5, + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "cells": [ + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000001", + "metadata": {}, + "source": [ + "# LangGraph + CoordiNode: Agent with graph memory\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/03_langgraph_agent.ipynb)\n", + "\n", + "Demonstrates a LangGraph agent that uses CoordiNode as persistent **graph memory**:\n", + "- `save_fact` — store a subject → relation → object triple in the graph\n", + "- `query_facts` — run an arbitrary Cypher query against the graph\n", + "- `find_related` — traverse the graph from a given entity\n", + "- `list_all_facts` — dump every fact in the current session\n", + "\n", + "**Works without OpenAI** — the mock demo section calls tools directly. \n", + "Set `OPENAI_API_KEY` to run the full `gpt-4o-mini` ReAct agent.\n", + "\n", + "**Environments:**\n", + "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", + "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC." + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000002", + "metadata": {}, + "source": [ + "## Install dependencies" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000003", + "metadata": {}, + "outputs": [], + "source": [ + "import sys, subprocess\n", + "\n", + "IN_COLAB = 'google.colab' in sys.modules\n", + "\n", + "if IN_COLAB:\n", + " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run(\n", + " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", + " shell=True, check=True\n", + " )\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", + " 'langchain',\n", + " 'langchain-openai',\n", + " 'langchain-community',\n", + " 'langgraph',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "else:\n", + " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", + " 'coordinode',\n", + " 'langchain',\n", + " 'langchain-openai',\n", + " 'langchain-community',\n", + " 'langgraph',\n", + " 'nest_asyncio',\n", + " ], check=True)\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print('SDK installed')" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Connect to CoordiNode\n", + "\n", + "This notebook uses `.cypher()` directly, which is identical on both `LocalClient`\n", + "(embedded) and `CoordinodeClient` (gRPC). No adapter needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "import os, socket, subprocess, time\n", + "\n", + "if IN_COLAB:\n", + " from coordinode_embedded import LocalClient\n", + " client = LocalClient(\":memory:\")\n", + " print('Using embedded LocalClient (in-process)')\n", + "else:\n", + " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", + " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + "\n", + " def _port_open(port):\n", + " try:\n", + " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", + " except OSError: return False\n", + "\n", + " if os.environ.get('COORDINODE_ADDR'):\n", + " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", + " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " elif _port_open(GRPC_PORT):\n", + " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " else:\n", + " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", + " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", + " capture_output=True, text=True)\n", + " if proc.returncode != 0:\n", + " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " container_id = proc.stdout.strip()\n", + " for _ in range(30):\n", + " if _port_open(GRPC_PORT): break\n", + " time.sleep(1)\n", + " else:\n", + " subprocess.run(['docker', 'stop', container_id])\n", + " raise RuntimeError('CoordiNode did not start in 30 s')\n", + " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + "\n", + " from coordinode import CoordinodeClient\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f'Connected to {COORDINODE_ADDR}')" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## 1. Define LangChain tools\n", + "\n", + "Each tool maps a natural-language action to a Cypher operation.\n", + "A `SESSION` UUID isolates this demo's data from other concurrent runs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "import os, uuid\n", + "from langchain_core.tools import tool\n", + "\n", + "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", + "\n", + "@tool\n", + "def save_fact(subject: str, relation: str, obj: str) -> str:\n", + " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", + " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " client.cypher(\n", + " 'MERGE (a:Entity {name: $s, session: $sess}) '\n", + " 'MERGE (b:Entity {name: $o, session: $sess}) '\n", + " 'MERGE (a)-[r:' + relation.upper().replace(' ', '_') + ']->(b)',\n", + " params={'s': subject, 'o': obj, 'sess': SESSION}\n", + " )\n", + " return f'Saved: {subject} -[{relation}]-> {obj}'\n", + "\n", + "@tool\n", + "def query_facts(cypher: str) -> str:\n", + " \"\"\"Run a Cypher query against the knowledge graph and return results.\n", + " Use $sess to filter to the current session: WHERE n.session = $sess\"\"\"\n", + " rows = client.cypher(cypher, params={'sess': SESSION})\n", + " return str(rows[:20]) if rows else 'No results'\n", + "\n", + "@tool\n", + "def find_related(entity_name: str, depth: int = 1) -> str:\n", + " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " rows = client.cypher(\n", + " 'MATCH (n:Entity {name: $name, session: $sess})-[r*1..' + str(min(depth, 3)) + ']->(m) '\n", + " 'RETURN m.name AS related, type(last(r)) AS via LIMIT 20',\n", + " params={'name': entity_name, 'sess': SESSION}\n", + " )\n", + " if not rows:\n", + " return f'No related entities found for {entity_name}'\n", + " return '\\n'.join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", + "@tool\n", + "def list_all_facts() -> str:\n", + " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", + " rows = client.cypher(\n", + " 'MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) '\n", + " 'RETURN a.name AS subject, type(r) AS relation, b.name AS object',\n", + " params={'sess': SESSION}\n", + " )\n", + " if not rows:\n", + " return 'No facts stored yet'\n", + " return '\\n'.join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "\n", + "tools = [save_fact, query_facts, find_related, list_all_facts]\n", + "print(f'Session: {SESSION}')\n", + "print('Tools:', [t.name for t in tools])" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## 2. Mock demo — no LLM required (direct tool calls)\n", + "\n", + "Shows the full graph memory workflow by calling the tools directly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "print('=== Saving facts ===')\n", + "print(save_fact.invoke({'subject': 'Alice', 'relation': 'WORKS_AT', 'obj': 'Acme Corp'}))\n", + "print(save_fact.invoke({'subject': 'Alice', 'relation': 'MANAGES', 'obj': 'Bob'}))\n", + "print(save_fact.invoke({'subject': 'Bob', 'relation': 'WORKS_AT', 'obj': 'Acme Corp'}))\n", + "print(save_fact.invoke({'subject': 'Acme Corp', 'relation': 'LOCATED_IN', 'obj': 'Berlin'}))\n", + "print(save_fact.invoke({'subject': 'Alice', 'relation': 'KNOWS', 'obj': 'Charlie'}))\n", + "print(save_fact.invoke({'subject': 'Charlie', 'relation': 'EXPERT_IN', 'obj': 'Machine Learning'}))\n", + "\n", + "print('\\n=== All facts in session ===')\n", + "print(list_all_facts.invoke({}))\n", + "\n", + "print('\\n=== Related to Alice (depth=1) ===')\n", + "print(find_related.invoke({'entity_name': 'Alice', 'depth': 1}))\n", + "\n", + "print('\\n=== Related to Alice (depth=2) ===')\n", + "print(find_related.invoke({'entity_name': 'Alice', 'depth': 2}))\n", + "\n", + "print('\\n=== Cypher query: who works at Acme Corp? ===')\n", + "print(query_facts.invoke({\n", + " 'cypher': 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\"}) RETURN p.name AS employee'\n", + "}))" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## 3. LangGraph StatefulGraph — manual workflow\n", + "\n", + "Shows how to wire CoordiNode tool calls into a LangGraph state machine without an LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.graph import StateGraph, END\n", + "from typing import TypedDict, Annotated\n", + "import operator\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[list[str], operator.add]\n", + " facts_saved: int\n", + "\n", + "def extract_and_save_node(state: AgentState) -> AgentState:\n", + " \"\"\"Simulates entity extraction: saves a hardcoded fact.\n", + " In production this node would call an LLM to extract entities from the last message.\"\"\"\n", + " result = save_fact.invoke({'subject': 'DemoSubject', 'relation': 'DEMO_REL', 'obj': 'DemoObject'})\n", + " return {'messages': [f'[extract] {result}'], 'facts_saved': state['facts_saved'] + 1}\n", + "\n", + "def query_node(state: AgentState) -> AgentState:\n", + " \"\"\"Reads the graph and appends a summary to messages.\"\"\"\n", + " result = list_all_facts.invoke({})\n", + " return {'messages': [f'[query] Facts: {result[:200]}'], 'facts_saved': state['facts_saved']}\n", + "\n", + "def should_query(state: AgentState) -> str:\n", + " return 'query' if state['facts_saved'] >= 1 else 'extract'\n", + "\n", + "builder = StateGraph(AgentState)\n", + "builder.add_node('extract', extract_and_save_node)\n", + "builder.add_node('query', query_node)\n", + "builder.set_entry_point('extract')\n", + "builder.add_conditional_edges('extract', should_query, {'query': 'query', 'extract': 'extract'})\n", + "builder.add_edge('query', END)\n", + "\n", + "graph_agent = builder.compile()\n", + "\n", + "result = graph_agent.invoke({'messages': ['Tell me about Alice'], 'facts_saved': 0})\n", + "print('Graph agent output:')\n", + "for msg in result['messages']:\n", + " print(' ', msg)" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000012", + "metadata": {}, + "source": [ + "## 4. LangGraph ReAct agent (optional — requires OPENAI_API_KEY)\n", + "\n", + "> Set `OPENAI_API_KEY` in your environment or via\n", + "> `os.environ['OPENAI_API_KEY'] = 'sk-...'` before running.\n", + "> The cell is skipped automatically when the key is absent." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000013", + "metadata": {}, + "outputs": [], + "source": [ + "if not os.environ.get('OPENAI_API_KEY'):\n", + " print('OPENAI_API_KEY not set — skipping LLM agent. See section 2 for the mock demo.')\n", + "else:\n", + " from langchain_openai import ChatOpenAI\n", + " from langgraph.prebuilt import create_react_agent\n", + "\n", + " llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)\n", + " agent = create_react_agent(llm, tools)\n", + "\n", + " print('Agent ready. Running demo conversation...')\n", + " messages = [\n", + " {'role': 'user', 'content': 'Save these facts: Alice works at Acme Corp, Alice manages Bob, Acme Corp is in Berlin.'},\n", + " {'role': 'user', 'content': 'Who does Alice manage?'},\n", + " {'role': 'user', 'content': 'What are all the facts about Alice?'},\n", + " ]\n", + " for msg in messages:\n", + " print(f'\\n>>> {msg[\"content\"]}')\n", + " result = agent.invoke({'messages': [msg]})\n", + " print('Agent:', result['messages'][-1].content)" + ] + }, + { + "cell_type": "markdown", + "id": "d4e5f6a7-0003-0000-0000-000000000014", + "metadata": {}, + "source": [ + "## Cleanup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4e5f6a7-0003-0000-0000-000000000015", + "metadata": {}, + "outputs": [], + "source": [ + "client.cypher('MATCH (n:Entity {session: $sess}) DETACH DELETE n', params={'sess': SESSION})\n", + "print('Cleaned up session:', SESSION)\n", + "client.close()" + ] + } + ] +} diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 54cc531..52abce3 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -45,8 +45,13 @@ def __init__( *, database: str | None = None, timeout: float = 30.0, + client: Any = None, ) -> None: - self._client = CoordinodeClient(addr, timeout=timeout) + # ``client`` allows passing a pre-built client (e.g. LocalClient from + # coordinode-embedded) instead of creating a gRPC connection. The object + # must expose a ``.cypher(query, params)`` method and, optionally, + # ``.get_schema_text()`` and ``.vector_search()``. + self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) self._schema: str | None = None self._structured_schema: dict[str, Any] | None = None diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 1a6b3e1..be545a9 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -68,8 +68,11 @@ def __init__( addr: str = "localhost:7080", *, timeout: float = 30.0, + client: Any = None, ) -> None: - self._client = CoordinodeClient(addr, timeout=timeout) + # ``client`` allows passing a pre-built client (e.g. LocalClient from + # coordinode-embedded) instead of creating a gRPC connection. + self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) # ── Node operations ─────────────────────────────────────────────────── From e9805bc28e83eb221124f0c9b9cf409031384e19 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 18:21:04 +0300 Subject: [PATCH 03/86] refactor(client): extract _build_property_definitions; fix unknown type validation - Extract shared _build_property_definitions() static method: both create_label and create_edge_type now call it, eliminating duplicated _type_map and property-building loop (addresses CodeRabbit #1) - Raise ValueError for unknown property type in both methods instead of silently coercing to STRING; error includes property name and valid options (addresses CodeRabbit #2, already in previous commit) - Clarify _first_label() docstring: nodes "typically have one label in this application" rather than asserting system-level single-label constraint (addresses CodeRabbit #3) --- coordinode/coordinode/client.py | 89 +++++++++---------- .../langchain_coordinode/graph.py | 9 +- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index ea96d04..daf8b6d 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -368,6 +368,46 @@ async def get_edge_types(self) -> list[EdgeTypeInfo]: resp = await self._schema_stub.ListEdgeTypes(ListEdgeTypesRequest(), timeout=self._timeout) return [EdgeTypeInfo(et) for et in resp.edge_types] + @staticmethod + def _build_property_definitions( + properties: list[dict[str, Any]] | None, + PropertyType: Any, + PropertyDefinition: Any, + ) -> list[Any]: + """Convert property dicts to proto PropertyDefinition objects. + + Shared by :meth:`create_label` and :meth:`create_edge_type` to avoid + duplicating the type-map and validation logic. + """ + type_map = { + "int64": PropertyType.PROPERTY_TYPE_INT64, + "float64": PropertyType.PROPERTY_TYPE_FLOAT64, + "string": PropertyType.PROPERTY_TYPE_STRING, + "bool": PropertyType.PROPERTY_TYPE_BOOL, + "bytes": PropertyType.PROPERTY_TYPE_BYTES, + "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, + "vector": PropertyType.PROPERTY_TYPE_VECTOR, + "list": PropertyType.PROPERTY_TYPE_LIST, + "map": PropertyType.PROPERTY_TYPE_MAP, + } + result = [] + for p in properties or []: + type_str = str(p.get("type", "string")).lower() + if type_str not in type_map: + raise ValueError( + f"Unknown property type {type_str!r} for property {p['name']!r}. " + f"Expected one of: {sorted(type_map)}" + ) + result.append( + PropertyDefinition( + name=p["name"], + type=type_map[type_str], + required=bool(p.get("required", False)), + unique=bool(p.get("unique", False)), + ) + ) + return result + async def create_label( self, name: str, @@ -395,17 +435,6 @@ async def create_label( SchemaMode, ) - _type_map = { - "int64": PropertyType.PROPERTY_TYPE_INT64, - "float64": PropertyType.PROPERTY_TYPE_FLOAT64, - "string": PropertyType.PROPERTY_TYPE_STRING, - "bool": PropertyType.PROPERTY_TYPE_BOOL, - "bytes": PropertyType.PROPERTY_TYPE_BYTES, - "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, - "vector": PropertyType.PROPERTY_TYPE_VECTOR, - "list": PropertyType.PROPERTY_TYPE_LIST, - "map": PropertyType.PROPERTY_TYPE_MAP, - } _mode_map = { "strict": SchemaMode.SCHEMA_MODE_STRICT, "validated": SchemaMode.SCHEMA_MODE_VALIDATED, @@ -414,18 +443,7 @@ async def create_label( if schema_mode not in _mode_map: raise ValueError(f"schema_mode must be one of {list(_mode_map)}, got {schema_mode!r}") - proto_props = [] - for p in properties or []: - type_str = str(p.get("type", "string")).lower() - proto_props.append( - PropertyDefinition( - name=p["name"], - type=_type_map.get(type_str, PropertyType.PROPERTY_TYPE_STRING), - required=bool(p.get("required", False)), - unique=bool(p.get("unique", False)), - ) - ) - + proto_props = self._build_property_definitions(properties, PropertyType, PropertyDefinition) req = CreateLabelRequest( name=name, properties=proto_props, @@ -453,30 +471,7 @@ async def create_edge_type( PropertyType, ) - _type_map = { - "int64": PropertyType.PROPERTY_TYPE_INT64, - "float64": PropertyType.PROPERTY_TYPE_FLOAT64, - "string": PropertyType.PROPERTY_TYPE_STRING, - "bool": PropertyType.PROPERTY_TYPE_BOOL, - "bytes": PropertyType.PROPERTY_TYPE_BYTES, - "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, - "vector": PropertyType.PROPERTY_TYPE_VECTOR, - "list": PropertyType.PROPERTY_TYPE_LIST, - "map": PropertyType.PROPERTY_TYPE_MAP, - } - - proto_props = [] - for p in properties or []: - type_str = str(p.get("type", "string")).lower() - proto_props.append( - PropertyDefinition( - name=p["name"], - type=_type_map.get(type_str, PropertyType.PROPERTY_TYPE_STRING), - required=bool(p.get("required", False)), - unique=bool(p.get("unique", False)), - ) - ) - + proto_props = self._build_property_definitions(properties, PropertyType, PropertyDefinition) req = CreateEdgeTypeRequest(name=name, properties=proto_props) et = await self._schema_stub.CreateEdgeType(req, timeout=self._timeout) return EdgeTypeInfo(et) diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 52abce3..521cc1e 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -272,9 +272,12 @@ def _stable_document_id(source: Any) -> str: def _first_label(labels: Any) -> str | None: """Extract a stable label from a labels() result (list of strings). - CoordiNode nodes have exactly one label, so labels() always returns a - single-element list. min() gives a deterministic result for robustness. - TODO: replace with labels(n)[0] in Cypher once subscript-on-function + In practice this application creates nodes with a single label, but the + underlying CoordiNode API accepts ``list[str]`` so multi-label nodes are + possible. ``min()`` gives a deterministic result regardless of how many + labels are present. + + TODO: replace with ``labels(n)[0]`` in Cypher once subscript-on-function lands in the published Docker image. """ if isinstance(labels, list) and labels: From fc8f53bb2d83f962fdf1e6a8a1e04cb11a8aab61 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 20:09:09 +0300 Subject: [PATCH 04/86] =?UTF-8?q?style:=20fix=20ruff=20lint/fmt=20?= =?UTF-8?q?=E2=80=94=20exclude=20submodule,=20notebook=20per-file=20ignore?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ruff.toml: exclude coordinode-rs/ (submodule) and **/.ipynb_checkpoints/ from lint+format; add per-file-ignores for *.ipynb (E401/E402/I001 — normal notebook patterns: multi-import on one line, import after pip install cell) - Auto-format demo notebooks and coordinode-embedded .pyi stub - Fix UP037 in _coordinode_embedded.pyi: remove quotes from type annotation - Add .ipynb_checkpoints/ to .gitignore --- .gitignore | 1 + .../_coordinode_embedded.pyi | 3 +- demo/notebooks/00_seed_data.ipynb | 322 +++++++++--------- .../01_llama_index_property_graph.ipynb | 209 +++++++----- demo/notebooks/02_langchain_graph_chain.ipynb | 221 ++++++------ demo/notebooks/03_langgraph_agent.ipynb | 271 ++++++++------- ruff.toml | 9 + 7 files changed, 574 insertions(+), 462 deletions(-) diff --git a/.gitignore b/.gitignore index 8f8786e..1800bea 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ langchain-coordinode/langchain_coordinode/_version.py llama-index-coordinode/llama_index/graph_stores/coordinode/_version.py GAPS.md CLAUDE.md +.ipynb_checkpoints/ diff --git a/coordinode-embedded/python/coordinode_embedded/_coordinode_embedded.pyi b/coordinode-embedded/python/coordinode_embedded/_coordinode_embedded.pyi index 77c14a2..c0711f4 100644 --- a/coordinode-embedded/python/coordinode_embedded/_coordinode_embedded.pyi +++ b/coordinode-embedded/python/coordinode_embedded/_coordinode_embedded.pyi @@ -23,7 +23,6 @@ class LocalClient: """ def __init__(self, path: str) -> None: ... - def cypher( self, query: str, @@ -48,6 +47,6 @@ class LocalClient: """ ... - def __enter__(self) -> "LocalClient": ... + def __enter__(self) -> LocalClient: ... def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool: ... def __repr__(self) -> str: ... diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index b91c88f..7b561e9 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -1,16 +1,4 @@ { - "nbformat": 4, - "nbformat_minor": 5, - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", @@ -55,30 +43,44 @@ "source": [ "import sys, subprocess\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", " subprocess.run(\n", - " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", - " shell=True, check=True\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", " )\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", "else:\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'coordinode',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " )\n", "\n", "import nest_asyncio\n", + "\n", "nest_asyncio.apply()\n", "\n", - "print('Ready')" + "print(\"Ready\")" ] }, { @@ -103,42 +105,48 @@ "\n", "if IN_COLAB:\n", " from coordinode_embedded import LocalClient\n", + "\n", " client = LocalClient(\":memory:\")\n", - " print('Using embedded LocalClient (in-process)')\n", + " print(\"Using embedded LocalClient (in-process)\")\n", "else:\n", - " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", - " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", "\n", " def _port_open(port):\n", " try:\n", - " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", - " except OSError: return False\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", - " if os.environ.get('COORDINODE_ADDR'):\n", - " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", - " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", " elif _port_open(GRPC_PORT):\n", - " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " else:\n", - " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", - " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", - " capture_output=True, text=True)\n", + " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", + " proc = subprocess.run(\n", + " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", + " )\n", " if proc.returncode != 0:\n", - " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", " container_id = proc.stdout.strip()\n", " for _ in range(30):\n", - " if _port_open(GRPC_PORT): break\n", + " if _port_open(GRPC_PORT):\n", + " break\n", " time.sleep(1)\n", " else:\n", - " subprocess.run(['docker', 'stop', container_id])\n", - " raise RuntimeError('CoordiNode did not start in 30 s')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", - " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + " subprocess.run([\"docker\", \"stop\", container_id])\n", + " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", "\n", " from coordinode import CoordinodeClient\n", + "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f'Connected to {COORDINODE_ADDR}')" + " print(f\"Connected to {COORDINODE_ADDR}\")" ] }, { @@ -156,8 +164,8 @@ "metadata": {}, "outputs": [], "source": [ - "result = client.cypher('MATCH (n {demo: true}) DETACH DELETE n')\n", - "print('Previous demo data removed')" + "result = client.cypher(\"MATCH (n {demo: true}) DETACH DELETE n\")\n", + "print(\"Previous demo data removed\")" ] }, { @@ -177,66 +185,57 @@ "source": [ "# ── People ────────────────────────────────────────────────────────────────\n", "people = [\n", - " {'name': 'Alice Chen', 'role': 'ML Researcher', 'org': 'DeepMind', 'field': 'Reinforcement Learning'},\n", - " {'name': 'Bob Torres', 'role': 'Staff Engineer', 'org': 'Google', 'field': 'Distributed Systems'},\n", - " {'name': 'Carol Smith', 'role': 'Founder & CEO', 'org': 'Synthex', 'field': 'NLP'},\n", - " {'name': 'David Park', 'role': 'Research Scientist', 'org': 'OpenAI', 'field': 'LLMs'},\n", - " {'name': 'Eva Müller', 'role': 'Systems Architect', 'org': 'Synthex', 'field': 'Graph Databases'},\n", - " {'name': 'Frank Liu', 'role': 'Principal Engineer', 'org': 'Meta', 'field': 'Graph ML'},\n", - " {'name': 'Grace Okafor', 'role': 'PhD Researcher', 'org': 'MIT', 'field': 'Knowledge Graphs'},\n", - " {'name': 'Henry Rossi', 'role': 'CTO', 'org': 'Synthex', 'field': 'Databases'},\n", - " {'name': 'Isla Nakamura', 'role': 'Senior Researcher', 'org': 'DeepMind', 'field': 'Graph Neural Networks'},\n", - " {'name': 'James Wright', 'role': 'Engineering Lead', 'org': 'Google', 'field': 'Search'},\n", + " {\"name\": \"Alice Chen\", \"role\": \"ML Researcher\", \"org\": \"DeepMind\", \"field\": \"Reinforcement Learning\"},\n", + " {\"name\": \"Bob Torres\", \"role\": \"Staff Engineer\", \"org\": \"Google\", \"field\": \"Distributed Systems\"},\n", + " {\"name\": \"Carol Smith\", \"role\": \"Founder & CEO\", \"org\": \"Synthex\", \"field\": \"NLP\"},\n", + " {\"name\": \"David Park\", \"role\": \"Research Scientist\", \"org\": \"OpenAI\", \"field\": \"LLMs\"},\n", + " {\"name\": \"Eva Müller\", \"role\": \"Systems Architect\", \"org\": \"Synthex\", \"field\": \"Graph Databases\"},\n", + " {\"name\": \"Frank Liu\", \"role\": \"Principal Engineer\", \"org\": \"Meta\", \"field\": \"Graph ML\"},\n", + " {\"name\": \"Grace Okafor\", \"role\": \"PhD Researcher\", \"org\": \"MIT\", \"field\": \"Knowledge Graphs\"},\n", + " {\"name\": \"Henry Rossi\", \"role\": \"CTO\", \"org\": \"Synthex\", \"field\": \"Databases\"},\n", + " {\"name\": \"Isla Nakamura\", \"role\": \"Senior Researcher\", \"org\": \"DeepMind\", \"field\": \"Graph Neural Networks\"},\n", + " {\"name\": \"James Wright\", \"role\": \"Engineering Lead\", \"org\": \"Google\", \"field\": \"Search\"},\n", "]\n", "\n", "for p in people:\n", - " client.cypher(\n", - " 'MERGE (n:Person {name: $name}) '\n", - " 'SET n.role = $role, n.field = $field, n.demo = true',\n", - " params=p\n", - " )\n", + " client.cypher(\"MERGE (n:Person {name: $name}) SET n.role = $role, n.field = $field, n.demo = true\", params=p)\n", "\n", - "print(f'Created {len(people)} people')\n", + "print(f\"Created {len(people)} people\")\n", "\n", "# ── Companies ─────────────────────────────────────────────────────────────\n", "companies = [\n", - " {'name': 'Google', 'industry': 'Technology', 'founded': 1998, 'hq': 'Mountain View'},\n", - " {'name': 'Meta', 'industry': 'Technology', 'founded': 2004, 'hq': 'Menlo Park'},\n", - " {'name': 'OpenAI', 'industry': 'AI Research', 'founded': 2015, 'hq': 'San Francisco'},\n", - " {'name': 'DeepMind', 'industry': 'AI Research', 'founded': 2010, 'hq': 'London'},\n", - " {'name': 'Synthex', 'industry': 'AI Startup', 'founded': 2021, 'hq': 'Berlin'},\n", - " {'name': 'MIT', 'industry': 'Academia', 'founded': 1861, 'hq': 'Cambridge'},\n", + " {\"name\": \"Google\", \"industry\": \"Technology\", \"founded\": 1998, \"hq\": \"Mountain View\"},\n", + " {\"name\": \"Meta\", \"industry\": \"Technology\", \"founded\": 2004, \"hq\": \"Menlo Park\"},\n", + " {\"name\": \"OpenAI\", \"industry\": \"AI Research\", \"founded\": 2015, \"hq\": \"San Francisco\"},\n", + " {\"name\": \"DeepMind\", \"industry\": \"AI Research\", \"founded\": 2010, \"hq\": \"London\"},\n", + " {\"name\": \"Synthex\", \"industry\": \"AI Startup\", \"founded\": 2021, \"hq\": \"Berlin\"},\n", + " {\"name\": \"MIT\", \"industry\": \"Academia\", \"founded\": 1861, \"hq\": \"Cambridge\"},\n", "]\n", "\n", "for c in companies:\n", " client.cypher(\n", - " 'MERGE (n:Company {name: $name}) '\n", - " 'SET n.industry = $industry, n.founded = $founded, n.hq = $hq, n.demo = true',\n", - " params=c\n", + " \"MERGE (n:Company {name: $name}) SET n.industry = $industry, n.founded = $founded, n.hq = $hq, n.demo = true\",\n", + " params=c,\n", " )\n", "\n", - "print(f'Created {len(companies)} companies')\n", + "print(f\"Created {len(companies)} companies\")\n", "\n", "# ── Technologies ──────────────────────────────────────────────────────────\n", "technologies = [\n", - " {'name': 'Transformer', 'type': 'Architecture', 'year': 2017},\n", - " {'name': 'Graph Neural Network', 'type': 'Algorithm', 'year': 2009},\n", - " {'name': 'Reinforcement Learning','type': 'Paradigm', 'year': 1980},\n", - " {'name': 'Knowledge Graph', 'type': 'Data Model', 'year': 2012},\n", - " {'name': 'Vector Database', 'type': 'Infrastructure','year': 2019},\n", - " {'name': 'RAG', 'type': 'Technique', 'year': 2020},\n", - " {'name': 'LLM', 'type': 'Model Class', 'year': 2018},\n", - " {'name': 'GraphRAG', 'type': 'Technique', 'year': 2023},\n", + " {\"name\": \"Transformer\", \"type\": \"Architecture\", \"year\": 2017},\n", + " {\"name\": \"Graph Neural Network\", \"type\": \"Algorithm\", \"year\": 2009},\n", + " {\"name\": \"Reinforcement Learning\", \"type\": \"Paradigm\", \"year\": 1980},\n", + " {\"name\": \"Knowledge Graph\", \"type\": \"Data Model\", \"year\": 2012},\n", + " {\"name\": \"Vector Database\", \"type\": \"Infrastructure\", \"year\": 2019},\n", + " {\"name\": \"RAG\", \"type\": \"Technique\", \"year\": 2020},\n", + " {\"name\": \"LLM\", \"type\": \"Model Class\", \"year\": 2018},\n", + " {\"name\": \"GraphRAG\", \"type\": \"Technique\", \"year\": 2023},\n", "]\n", "\n", "for t in technologies:\n", - " client.cypher(\n", - " 'MERGE (n:Technology {name: $name}) '\n", - " 'SET n.type = $type, n.year = $year, n.demo = true',\n", - " params=t\n", - " )\n", + " client.cypher(\"MERGE (n:Technology {name: $name}) SET n.type = $type, n.year = $year, n.demo = true\", params=t)\n", "\n", - "print(f'Created {len(technologies)} technologies')" + "print(f\"Created {len(technologies)} technologies\")" ] }, { @@ -256,70 +255,74 @@ "source": [ "edges = [\n", " # WORKS_AT\n", - " ('Alice Chen', 'WORKS_AT', 'DeepMind', {}),\n", - " ('Bob Torres', 'WORKS_AT', 'Google', {}),\n", - " ('Carol Smith', 'WORKS_AT', 'Synthex', {'since': 2021}),\n", - " ('David Park', 'WORKS_AT', 'OpenAI', {}),\n", - " ('Eva Müller', 'WORKS_AT', 'Synthex', {'since': 2022}),\n", - " ('Frank Liu', 'WORKS_AT', 'Meta', {}),\n", - " ('Grace Okafor', 'WORKS_AT', 'MIT', {}),\n", - " ('Henry Rossi', 'WORKS_AT', 'Synthex', {'since': 2021}),\n", - " ('Isla Nakamura', 'WORKS_AT', 'DeepMind', {}),\n", - " ('James Wright', 'WORKS_AT', 'Google', {}),\n", + " (\"Alice Chen\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"Bob Torres\", \"WORKS_AT\", \"Google\", {}),\n", + " (\"Carol Smith\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"David Park\", \"WORKS_AT\", \"OpenAI\", {}),\n", + " (\"Eva Müller\", \"WORKS_AT\", \"Synthex\", {\"since\": 2022}),\n", + " (\"Frank Liu\", \"WORKS_AT\", \"Meta\", {}),\n", + " (\"Grace Okafor\", \"WORKS_AT\", \"MIT\", {}),\n", + " (\"Henry Rossi\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"James Wright\", \"WORKS_AT\", \"Google\", {}),\n", " # FOUNDED\n", - " ('Carol Smith', 'FOUNDED', 'Synthex', {'year': 2021}),\n", - " ('Henry Rossi', 'CO_FOUNDED', 'Synthex', {'year': 2021}),\n", + " (\"Carol Smith\", \"FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", + " (\"Henry Rossi\", \"CO_FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", " # KNOWS\n", - " ('Alice Chen', 'KNOWS', 'Isla Nakamura', {}),\n", - " ('Alice Chen', 'KNOWS', 'David Park', {}),\n", - " ('Carol Smith', 'KNOWS', 'Bob Torres', {}),\n", - " ('Grace Okafor', 'KNOWS', 'Alice Chen', {}),\n", - " ('Frank Liu', 'KNOWS', 'James Wright', {}),\n", - " ('Eva Müller', 'KNOWS', 'Grace Okafor', {}),\n", + " (\"Alice Chen\", \"KNOWS\", \"Isla Nakamura\", {}),\n", + " (\"Alice Chen\", \"KNOWS\", \"David Park\", {}),\n", + " (\"Carol Smith\", \"KNOWS\", \"Bob Torres\", {}),\n", + " (\"Grace Okafor\", \"KNOWS\", \"Alice Chen\", {}),\n", + " (\"Frank Liu\", \"KNOWS\", \"James Wright\", {}),\n", + " (\"Eva Müller\", \"KNOWS\", \"Grace Okafor\", {}),\n", " # RESEARCHES / WORKS_ON\n", - " ('Alice Chen', 'RESEARCHES', 'Reinforcement Learning', {'since': 2019}),\n", - " ('David Park', 'RESEARCHES', 'LLM', {'since': 2020}),\n", - " ('Grace Okafor', 'RESEARCHES', 'Knowledge Graph', {'since': 2021}),\n", - " ('Isla Nakamura', 'RESEARCHES', 'Graph Neural Network', {'since': 2020}),\n", - " ('Frank Liu', 'RESEARCHES', 'Graph Neural Network', {}),\n", - " ('Grace Okafor', 'RESEARCHES', 'GraphRAG', {'since': 2023}),\n", + " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", + " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"Knowledge Graph\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"RESEARCHES\", \"Graph Neural Network\", {\"since\": 2020}),\n", + " (\"Frank Liu\", \"RESEARCHES\", \"Graph Neural Network\", {}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"GraphRAG\", {\"since\": 2023}),\n", " # USES\n", - " ('Synthex', 'USES', 'Knowledge Graph', {}),\n", - " ('Synthex', 'USES', 'Vector Database', {}),\n", - " ('Synthex', 'USES', 'RAG', {}),\n", - " ('OpenAI', 'USES', 'Transformer', {}),\n", - " ('Google', 'USES', 'Transformer', {}),\n", + " (\"Synthex\", \"USES\", \"Knowledge Graph\", {}),\n", + " (\"Synthex\", \"USES\", \"Vector Database\", {}),\n", + " (\"Synthex\", \"USES\", \"RAG\", {}),\n", + " (\"OpenAI\", \"USES\", \"Transformer\", {}),\n", + " (\"Google\", \"USES\", \"Transformer\", {}),\n", " # ACQUIRED\n", - " ('Google', 'ACQUIRED', 'DeepMind', {'year': 2014}),\n", + " (\"Google\", \"ACQUIRED\", \"DeepMind\", {\"year\": 2014}),\n", " # BUILDS_ON\n", - " ('GraphRAG', 'BUILDS_ON', 'Knowledge Graph', {}),\n", - " ('GraphRAG', 'BUILDS_ON', 'RAG', {}),\n", - " ('RAG', 'BUILDS_ON', 'Vector Database', {}),\n", - " ('LLM', 'BUILDS_ON', 'Transformer', {}),\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"Knowledge Graph\", {}),\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"RAG\", {}),\n", + " (\"RAG\", \"BUILDS_ON\", \"Vector Database\", {}),\n", + " (\"LLM\", \"BUILDS_ON\", \"Transformer\", {}),\n", "]\n", "\n", - "src_names = {p['name'] for p in people}\n", - "tech_names = {t['name'] for t in technologies}\n", - "company_names = {c['name'] for c in companies}\n", + "src_names = {p[\"name\"] for p in people}\n", + "tech_names = {t[\"name\"] for t in technologies}\n", + "company_names = {c[\"name\"] for c in companies}\n", + "\n", "\n", "def _label(name):\n", - " if name in src_names: return 'Person'\n", - " if name in tech_names: return 'Technology'\n", - " return 'Company'\n", + " if name in src_names:\n", + " return \"Person\"\n", + " if name in tech_names:\n", + " return \"Technology\"\n", + " return \"Company\"\n", + "\n", "\n", "for src, rel, dst, props in edges:\n", " src_label = _label(src)\n", " dst_label = _label(dst)\n", - " set_clause = ', '.join(f'r.{k} = ${k}' for k in props) if props else ''\n", - " set_part = f' SET {set_clause}' if set_clause else ''\n", + " set_clause = \", \".join(f\"r.{k} = ${k}\" for k in props) if props else \"\"\n", + " set_part = f\" SET {set_clause}\" if set_clause else \"\"\n", " client.cypher(\n", - " f'MATCH (a:{src_label} {{name: $src}}) '\n", - " f'MATCH (b:{dst_label} {{name: $dst}}) '\n", - " f'MERGE (a)-[r:{rel}]->(b)' + set_part,\n", - " params={'src': src, 'dst': dst, **props}\n", + " f\"MATCH (a:{src_label} {{name: $src}}) \"\n", + " f\"MATCH (b:{dst_label} {{name: $dst}}) \"\n", + " f\"MERGE (a)-[r:{rel}]->(b)\" + set_part,\n", + " params={\"src\": src, \"dst\": dst, **props},\n", " )\n", "\n", - "print(f'Created {len(edges)} relationships')" + "print(f\"Created {len(edges)} relationships\")" ] }, { @@ -339,19 +342,17 @@ "source": [ "from collections import Counter\n", "\n", - "print('Node counts:')\n", - "for label in ['Person', 'Company', 'Technology']:\n", - " rows = client.cypher(\n", - " f'MATCH (n:{label} {{demo: true}}) RETURN count(n) AS count'\n", - " )\n", + "print(\"Node counts:\")\n", + "for label in [\"Person\", \"Company\", \"Technology\"]:\n", + " rows = client.cypher(f\"MATCH (n:{label} {{demo: true}}) RETURN count(n) AS count\")\n", " print(f\" {label:15s} {rows[0]['count']}\")\n", "\n", "# Fetch all types and count in Python (avoids aggregation limitations)\n", - "rels = client.cypher('MATCH (a {demo: true})-[r]->(b) RETURN type(r) AS rel')\n", - "counts = Counter(r['rel'] for r in rels)\n", - "print('\\nRelationship counts:')\n", + "rels = client.cypher(\"MATCH (a {demo: true})-[r]->(b) RETURN type(r) AS rel\")\n", + "counts = Counter(r[\"rel\"] for r in rels)\n", + "print(\"\\nRelationship counts:\")\n", "for rel, cnt in sorted(counts.items(), key=lambda x: -x[1]):\n", - " print(f' {rel:20s} {cnt}')" + " print(f\" {rel:20s} {cnt}\")" ] }, { @@ -361,33 +362,44 @@ "metadata": {}, "outputs": [], "source": [ - "print('=== Who works at Synthex? ===')\n", + "print(\"=== Who works at Synthex? ===\")\n", "rows = client.cypher(\n", - " 'MATCH (p:Person)-[:WORKS_AT]->(c:Company {name: $co}) RETURN p.name AS name, p.role AS role',\n", - " params={'co': 'Synthex'}\n", + " \"MATCH (p:Person)-[:WORKS_AT]->(c:Company {name: $co}) RETURN p.name AS name, p.role AS role\",\n", + " params={\"co\": \"Synthex\"},\n", ")\n", "for r in rows:\n", " print(f\" {r['name']} — {r['role']}\")\n", "\n", - "print('\\n=== What does Synthex use? ===')\n", + "print(\"\\n=== What does Synthex use? ===\")\n", "rows = client.cypher(\n", - " 'MATCH (c:Company {name: $co})-[:USES]->(t:Technology) RETURN t.name AS name',\n", - " params={'co': 'Synthex'}\n", + " \"MATCH (c:Company {name: $co})-[:USES]->(t:Technology) RETURN t.name AS name\", params={\"co\": \"Synthex\"}\n", ")\n", "for r in rows:\n", " print(f\" {r['name']}\")\n", "\n", - "print('\\n=== GraphRAG dependency chain ===')\n", + "print(\"\\n=== GraphRAG dependency chain ===\")\n", "rows = client.cypher(\n", - " 'MATCH (t:Technology {name: $tech})-[:BUILDS_ON*1..3]->(dep) RETURN dep.name AS dependency',\n", - " params={'tech': 'GraphRAG'}\n", + " \"MATCH (t:Technology {name: $tech})-[:BUILDS_ON*1..3]->(dep) RETURN dep.name AS dependency\",\n", + " params={\"tech\": \"GraphRAG\"},\n", ")\n", "for r in rows:\n", " print(f\" → {r['dependency']}\")\n", "\n", - "print('\\n✓ Demo data ready — open notebooks 01, 02, 03 to explore!')\n", + "print(\"\\n✓ Demo data ready — open notebooks 01, 02, 03 to explore!\")\n", "client.close()" ] } - ] + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index a653a42..ebd8799 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -1,16 +1,4 @@ { - "nbformat": 4, - "nbformat_minor": 5, - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", @@ -54,34 +42,48 @@ "source": [ "import sys, subprocess\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", " subprocess.run(\n", - " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", - " shell=True, check=True\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", " )\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", - " 'llama-index-graph-stores-coordinode',\n", - " 'llama-index-core',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", "else:\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'coordinode',\n", - " 'llama-index-graph-stores-coordinode',\n", - " 'llama-index-core',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " )\n", "\n", "import nest_asyncio\n", + "\n", "nest_asyncio.apply()\n", "\n", - "print('SDK installed')" + "print(\"SDK installed\")" ] }, { @@ -105,6 +107,7 @@ "source": [ "class _EmbeddedAdapter:\n", " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + "\n", " def __init__(self, local_client):\n", " self._lc = local_client\n", "\n", @@ -112,22 +115,27 @@ " return self._lc.cypher(query, params or {})\n", "\n", " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\n", - " 'MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl'\n", - " )\n", - " rels = self._lc.cypher(\n", - " 'MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t'\n", - " )\n", - " lines = ['Node labels:']\n", - " for r in lbls: lines.append(f\" - {r['lbl']}\")\n", - " lines.append('\\nEdge types:')\n", - " for r in rels: lines.append(f\" - {r['t']}\")\n", - " return '\\n'.join(lines)\n", + " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", + " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", + " lines = [\"Node labels:\"]\n", + " for r in lbls:\n", + " lines.append(f\" - {r['lbl']}\")\n", + " lines.append(\"\\nEdge types:\")\n", + " for r in rels:\n", + " lines.append(f\" - {r['t']}\")\n", + " return \"\\n\".join(lines)\n", "\n", - " def vector_search(self, **kwargs): return []\n", - " def close(self): pass\n", - " def get_labels(self): return []\n", - " def get_edge_types(self): return []" + " def vector_search(self, **kwargs):\n", + " return []\n", + "\n", + " def close(self):\n", + " pass\n", + "\n", + " def get_labels(self):\n", + " return []\n", + "\n", + " def get_edge_types(self):\n", + " return []" ] }, { @@ -149,43 +157,49 @@ "\n", "if IN_COLAB:\n", " from coordinode_embedded import LocalClient\n", + "\n", " _lc = LocalClient(\":memory:\")\n", " client = _EmbeddedAdapter(_lc)\n", - " print('Using embedded LocalClient (in-process)')\n", + " print(\"Using embedded LocalClient (in-process)\")\n", "else:\n", - " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", - " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", "\n", " def _port_open(port):\n", " try:\n", - " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", - " except OSError: return False\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", - " if os.environ.get('COORDINODE_ADDR'):\n", - " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", - " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", " elif _port_open(GRPC_PORT):\n", - " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " else:\n", - " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", - " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", - " capture_output=True, text=True)\n", + " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", + " proc = subprocess.run(\n", + " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", + " )\n", " if proc.returncode != 0:\n", - " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", " container_id = proc.stdout.strip()\n", " for _ in range(30):\n", - " if _port_open(GRPC_PORT): break\n", + " if _port_open(GRPC_PORT):\n", + " break\n", " time.sleep(1)\n", " else:\n", - " subprocess.run(['docker', 'stop', container_id])\n", - " raise RuntimeError('CoordiNode did not start in 30 s')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", - " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + " subprocess.run([\"docker\", \"stop\", container_id])\n", + " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", "\n", " from coordinode import CoordinodeClient\n", + "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f'Connected to {COORDINODE_ADDR}')" + " print(f\"Connected to {COORDINODE_ADDR}\")" ] }, { @@ -209,7 +223,7 @@ "from llama_index.core.graph_stores.types import EntityNode, Relation\n", "\n", "store = CoordinodePropertyGraphStore(client=client)\n", - "print('Connected. Schema:')\n", + "print(\"Connected. Schema:\")\n", "print(store.get_schema()[:300])" ] }, @@ -231,24 +245,25 @@ "outputs": [], "source": [ "import uuid\n", + "\n", "tag = uuid.uuid4().hex[:6]\n", "\n", "nodes = [\n", - " EntityNode(label='Person', name=f'Alice-{tag}', properties={'role': 'researcher', 'field': 'AI'}),\n", - " EntityNode(label='Person', name=f'Bob-{tag}', properties={'role': 'engineer', 'field': 'ML'}),\n", - " EntityNode(label='Topic', name=f'GraphRAG-{tag}', properties={'domain': 'knowledge graphs'}),\n", + " EntityNode(label=\"Person\", name=f\"Alice-{tag}\", properties={\"role\": \"researcher\", \"field\": \"AI\"}),\n", + " EntityNode(label=\"Person\", name=f\"Bob-{tag}\", properties={\"role\": \"engineer\", \"field\": \"ML\"}),\n", + " EntityNode(label=\"Topic\", name=f\"GraphRAG-{tag}\", properties={\"domain\": \"knowledge graphs\"}),\n", "]\n", "store.upsert_nodes(nodes)\n", - "print('Upserted nodes:', [n.name for n in nodes])\n", + "print(\"Upserted nodes:\", [n.name for n in nodes])\n", "\n", "alice, bob, graphrag = nodes\n", "relations = [\n", - " Relation(label='RESEARCHES', source_id=alice.id, target_id=graphrag.id, properties={'since': 2023}),\n", - " Relation(label='COLLABORATES', source_id=alice.id, target_id=bob.id),\n", - " Relation(label='IMPLEMENTS', source_id=bob.id, target_id=graphrag.id),\n", + " Relation(label=\"RESEARCHES\", source_id=alice.id, target_id=graphrag.id, properties={\"since\": 2023}),\n", + " Relation(label=\"COLLABORATES\", source_id=alice.id, target_id=bob.id),\n", + " Relation(label=\"IMPLEMENTS\", source_id=bob.id, target_id=graphrag.id),\n", "]\n", "store.upsert_relations(relations)\n", - "print('Upserted relations:', [r.label for r in relations])" + "print(\"Upserted relations:\", [r.label for r in relations])" ] }, { @@ -266,10 +281,10 @@ "metadata": {}, "outputs": [], "source": [ - "triplets = store.get_triplets(entity_names=[f'Alice-{tag}'])\n", - "print(f'Triplets for Alice-{tag}:')\n", + "triplets = store.get_triplets(entity_names=[f\"Alice-{tag}\"])\n", + "print(f\"Triplets for Alice-{tag}:\")\n", "for src, rel, dst in triplets:\n", - " print(f' {src.name} --[{rel.label}]--> {dst.name}')" + " print(f\" {src.name} --[{rel.label}]--> {dst.name}\")" ] }, { @@ -287,11 +302,11 @@ "metadata": {}, "outputs": [], "source": [ - "found_alice = store.get(properties={'name': f'Alice-{tag}'})\n", + "found_alice = store.get(properties={\"name\": f\"Alice-{tag}\"})\n", "rel_map = store.get_rel_map(found_alice, depth=1, limit=20)\n", - "print(f'Rel map for Alice-{tag} ({len(rel_map)} rows):')\n", + "print(f\"Rel map for Alice-{tag} ({len(rel_map)} rows):\")\n", "for src, rel, dst in rel_map:\n", - " print(f' {src.name} --[{rel.label}]--> {dst.name}')" + " print(f\" {src.name} --[{rel.label}]--> {dst.name}\")" ] }, { @@ -310,12 +325,12 @@ "outputs": [], "source": [ "rows = store.structured_query(\n", - " 'MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)'\n", - " ' WHERE p.name STARTS WITH $prefix'\n", - " ' RETURN p.name AS person, t.name AS topic, r.since AS since',\n", - " param_map={'prefix': f'Alice-{tag[:4]}'}\n", + " \"MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)\"\n", + " \" WHERE p.name STARTS WITH $prefix\"\n", + " \" RETURN p.name AS person, t.name AS topic, r.since AS since\",\n", + " param_map={\"prefix\": f\"Alice-{tag[:4]}\"},\n", ")\n", - "print('Query result:', rows)" + "print(\"Query result:\", rows)" ] }, { @@ -354,10 +369,10 @@ "source": [ "store.upsert_relations(relations) # second call — should still be exactly 1 edge\n", "rows = store.structured_query(\n", - " 'MATCH (a {name: $src})-[r:RESEARCHES]->(b {name: $dst}) RETURN count(r) AS cnt',\n", - " param_map={'src': f'Alice-{tag}', 'dst': f'GraphRAG-{tag}'}\n", + " \"MATCH (a {name: $src})-[r:RESEARCHES]->(b {name: $dst}) RETURN count(r) AS cnt\",\n", + " param_map={\"src\": f\"Alice-{tag}\", \"dst\": f\"GraphRAG-{tag}\"},\n", ")\n", - "print('Edge count after double upsert (expect 1):', rows[0]['cnt'])" + "print(\"Edge count after double upsert (expect 1):\", rows[0][\"cnt\"])" ] }, { @@ -375,10 +390,22 @@ "metadata": {}, "outputs": [], "source": [ - "store.delete(entity_names=[f'Alice-{tag}', f'Bob-{tag}', f'GraphRAG-{tag}'])\n", - "print('Cleaned up')\n", + "store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n", + "print(\"Cleaned up\")\n", "store.close()" ] } - ] + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 6f6154e..7e4d6cb 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -1,16 +1,4 @@ { - "nbformat": 4, - "nbformat_minor": 5, - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", @@ -51,38 +39,52 @@ "source": [ "import sys, subprocess\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", " subprocess.run(\n", - " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", - " shell=True, check=True\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain-coordinode\",\n", + " \"langchain\",\n", + " \"langchain-openai\",\n", + " \"langchain-community\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", " )\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", - " 'langchain-coordinode',\n", - " 'langchain',\n", - " 'langchain-openai',\n", - " 'langchain-community',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", "else:\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'coordinode',\n", - " 'langchain-coordinode',\n", - " 'langchain',\n", - " 'langchain-openai',\n", - " 'langchain-community',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain-coordinode\",\n", + " \"langchain\",\n", + " \"langchain-openai\",\n", + " \"langchain-community\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " )\n", "\n", "import nest_asyncio\n", + "\n", "nest_asyncio.apply()\n", "\n", - "print('SDK installed')" + "print(\"SDK installed\")" ] }, { @@ -106,6 +108,7 @@ "source": [ "class _EmbeddedAdapter:\n", " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + "\n", " def __init__(self, local_client):\n", " self._lc = local_client\n", "\n", @@ -113,22 +116,27 @@ " return self._lc.cypher(query, params or {})\n", "\n", " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\n", - " 'MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl'\n", - " )\n", - " rels = self._lc.cypher(\n", - " 'MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t'\n", - " )\n", - " lines = ['Node labels:']\n", - " for r in lbls: lines.append(f\" - {r['lbl']}\")\n", - " lines.append('\\nEdge types:')\n", - " for r in rels: lines.append(f\" - {r['t']}\")\n", - " return '\\n'.join(lines)\n", + " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", + " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", + " lines = [\"Node labels:\"]\n", + " for r in lbls:\n", + " lines.append(f\" - {r['lbl']}\")\n", + " lines.append(\"\\nEdge types:\")\n", + " for r in rels:\n", + " lines.append(f\" - {r['t']}\")\n", + " return \"\\n\".join(lines)\n", "\n", - " def vector_search(self, **kwargs): return []\n", - " def close(self): pass\n", - " def get_labels(self): return []\n", - " def get_edge_types(self): return []" + " def vector_search(self, **kwargs):\n", + " return []\n", + "\n", + " def close(self):\n", + " pass\n", + "\n", + " def get_labels(self):\n", + " return []\n", + "\n", + " def get_edge_types(self):\n", + " return []" ] }, { @@ -150,43 +158,49 @@ "\n", "if IN_COLAB:\n", " from coordinode_embedded import LocalClient\n", + "\n", " _lc = LocalClient(\":memory:\")\n", " client = _EmbeddedAdapter(_lc)\n", - " print('Using embedded LocalClient (in-process)')\n", + " print(\"Using embedded LocalClient (in-process)\")\n", "else:\n", - " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", - " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", "\n", " def _port_open(port):\n", " try:\n", - " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", - " except OSError: return False\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", - " if os.environ.get('COORDINODE_ADDR'):\n", - " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", - " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", " elif _port_open(GRPC_PORT):\n", - " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " else:\n", - " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", - " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", - " capture_output=True, text=True)\n", + " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", + " proc = subprocess.run(\n", + " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", + " )\n", " if proc.returncode != 0:\n", - " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", " container_id = proc.stdout.strip()\n", " for _ in range(30):\n", - " if _port_open(GRPC_PORT): break\n", + " if _port_open(GRPC_PORT):\n", + " break\n", " time.sleep(1)\n", " else:\n", - " subprocess.run(['docker', 'stop', container_id])\n", - " raise RuntimeError('CoordiNode did not start in 30 s')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", - " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + " subprocess.run([\"docker\", \"stop\", container_id])\n", + " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", "\n", " from coordinode import CoordinodeClient\n", + "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f'Connected to {COORDINODE_ADDR}')" + " print(f\"Connected to {COORDINODE_ADDR}\")" ] }, { @@ -212,7 +226,7 @@ "from langchain_core.documents import Document\n", "\n", "graph = CoordinodeGraph(client=client)\n", - "print('Connected. Schema preview:')\n", + "print(\"Connected. Schema preview:\")\n", "print(graph.schema[:300])" ] }, @@ -237,18 +251,18 @@ "tag = uuid.uuid4().hex[:6]\n", "\n", "nodes = [\n", - " Node(id=f'Turing-{tag}', type='Scientist', properties={'born': 1912, 'field': 'computer science'}),\n", - " Node(id=f'Shannon-{tag}', type='Scientist', properties={'born': 1916, 'field': 'information theory'}),\n", - " Node(id=f'Cryptography-{tag}', type='Field', properties={'era': 'modern'}),\n", + " Node(id=f\"Turing-{tag}\", type=\"Scientist\", properties={\"born\": 1912, \"field\": \"computer science\"}),\n", + " Node(id=f\"Shannon-{tag}\", type=\"Scientist\", properties={\"born\": 1916, \"field\": \"information theory\"}),\n", + " Node(id=f\"Cryptography-{tag}\", type=\"Field\", properties={\"era\": \"modern\"}),\n", "]\n", "rels = [\n", - " Relationship(source=nodes[0], target=nodes[2], type='FOUNDED', properties={'year': 1936}),\n", - " Relationship(source=nodes[1], target=nodes[2], type='CONTRIBUTED_TO'),\n", - " Relationship(source=nodes[0], target=nodes[1], type='INFLUENCED'),\n", + " Relationship(source=nodes[0], target=nodes[2], type=\"FOUNDED\", properties={\"year\": 1936}),\n", + " Relationship(source=nodes[1], target=nodes[2], type=\"CONTRIBUTED_TO\"),\n", + " Relationship(source=nodes[0], target=nodes[1], type=\"INFLUENCED\"),\n", "]\n", - "doc = GraphDocument(nodes=nodes, relationships=rels, source=Document(page_content='Turing and Shannon'))\n", + "doc = GraphDocument(nodes=nodes, relationships=rels, source=Document(page_content=\"Turing and Shannon\"))\n", "graph.add_graph_documents([doc])\n", - "print('Documents added')" + "print(\"Documents added\")" ] }, { @@ -267,12 +281,12 @@ "outputs": [], "source": [ "rows = graph.query(\n", - " 'MATCH (s:Scientist)-[r]->(f:Field)'\n", - " ' WHERE s.name STARTS WITH $prefix'\n", - " ' RETURN s.name AS scientist, type(r) AS relation, f.name AS field',\n", - " params={'prefix': f'Turing-{tag[:4]}'}\n", + " \"MATCH (s:Scientist)-[r]->(f:Field)\"\n", + " \" WHERE s.name STARTS WITH $prefix\"\n", + " \" RETURN s.name AS scientist, type(r) AS relation, f.name AS field\",\n", + " params={\"prefix\": f\"Turing-{tag[:4]}\"},\n", ")\n", - "print('Scientists → Fields:')\n", + "print(\"Scientists → Fields:\")\n", "for r in rows:\n", " print(f\" {r['scientist']} --[{r['relation']}]--> {r['field']}\")" ] @@ -293,8 +307,8 @@ "outputs": [], "source": [ "graph.refresh_schema()\n", - "print('node_props keys:', list(graph.structured_schema.get('node_props', {}).keys())[:10])\n", - "print('relationships:', graph.structured_schema.get('relationships', [])[:5])" + "print(\"node_props keys:\", list(graph.structured_schema.get(\"node_props\", {}).keys())[:10])\n", + "print(\"relationships:\", graph.structured_schema.get(\"relationships\", [])[:5])" ] }, { @@ -317,10 +331,10 @@ "source": [ "graph.add_graph_documents([doc]) # second upsert — must not create a duplicate edge\n", "cnt = graph.query(\n", - " 'MATCH (a {name: $src})-[r:FOUNDED]->(b {name: $dst}) RETURN count(r) AS cnt',\n", - " params={'src': f'Turing-{tag}', 'dst': f'Cryptography-{tag}'}\n", + " \"MATCH (a {name: $src})-[r:FOUNDED]->(b {name: $dst}) RETURN count(r) AS cnt\",\n", + " params={\"src\": f\"Turing-{tag}\", \"dst\": f\"Cryptography-{tag}\"},\n", ")\n", - "print('FOUNDED edge count after double upsert (expect 1):', cnt[0]['cnt'])" + "print(\"FOUNDED edge count after double upsert (expect 1):\", cnt[0][\"cnt\"])" ] }, { @@ -342,20 +356,21 @@ "metadata": {}, "outputs": [], "source": [ - "if not os.environ.get('OPENAI_API_KEY'):\n", - " print('Skipping: OPENAI_API_KEY is not set.'\n", - " ' Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.')\n", + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " print(\n", + " 'Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.'\n", + " )\n", "else:\n", " from langchain.chains import GraphCypherQAChain\n", " from langchain_openai import ChatOpenAI\n", "\n", " chain = GraphCypherQAChain.from_llm(\n", - " ChatOpenAI(model='gpt-4o-mini', temperature=0),\n", + " ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n", " graph=graph,\n", " verbose=True,\n", " )\n", - " result = chain.invoke('Who influenced Shannon?')\n", - " print('Answer:', result['result'])" + " result = chain.invoke(\"Who influenced Shannon?\")\n", + " print(\"Answer:\", result[\"result\"])" ] }, { @@ -373,10 +388,22 @@ "metadata": {}, "outputs": [], "source": [ - "graph.query('MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n', params={'tag': tag})\n", - "print('Cleaned up')\n", + "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\n", + "print(\"Cleaned up\")\n", "graph.close()" ] } - ] + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 7e01614..b731ed0 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -1,16 +1,4 @@ { - "nbformat": 4, - "nbformat_minor": 5, - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python" - } - }, "cells": [ { "cell_type": "markdown", @@ -52,38 +40,52 @@ "source": [ "import sys, subprocess\n", "\n", - "IN_COLAB = 'google.colab' in sys.modules\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", + " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", " subprocess.run(\n", - " 'curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q',\n", - " shell=True, check=True\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain\",\n", + " \"langchain-openai\",\n", + " \"langchain-community\",\n", + " \"langgraph\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", " )\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q', 'maturin'], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded',\n", - " 'langchain',\n", - " 'langchain-openai',\n", - " 'langchain-community',\n", - " 'langgraph',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", "else:\n", - " subprocess.run([sys.executable, '-m', 'pip', 'install', '-q',\n", - " 'coordinode',\n", - " 'langchain',\n", - " 'langchain-openai',\n", - " 'langchain-community',\n", - " 'langgraph',\n", - " 'nest_asyncio',\n", - " ], check=True)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain\",\n", + " \"langchain-openai\",\n", + " \"langchain-community\",\n", + " \"langgraph\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " )\n", "\n", "import nest_asyncio\n", + "\n", "nest_asyncio.apply()\n", "\n", - "print('SDK installed')" + "print(\"SDK installed\")" ] }, { @@ -108,42 +110,48 @@ "\n", "if IN_COLAB:\n", " from coordinode_embedded import LocalClient\n", + "\n", " client = LocalClient(\":memory:\")\n", - " print('Using embedded LocalClient (in-process)')\n", + " print(\"Using embedded LocalClient (in-process)\")\n", "else:\n", - " GRPC_PORT = int(os.environ.get('COORDINODE_PORT', '7080'))\n", - " IMAGE = 'ghcr.io/structured-world/coordinode:latest'\n", + " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", "\n", " def _port_open(port):\n", " try:\n", - " with socket.create_connection(('127.0.0.1', port), timeout=1): return True\n", - " except OSError: return False\n", - "\n", - " if os.environ.get('COORDINODE_ADDR'):\n", - " COORDINODE_ADDR = os.environ['COORDINODE_ADDR']\n", - " print(f'Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}')\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + " if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", " elif _port_open(GRPC_PORT):\n", - " print(f'CoordiNode already reachable on :{GRPC_PORT}')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", + " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " else:\n", - " print(f'Starting CoordiNode via Docker on :{GRPC_PORT} …')\n", - " proc = subprocess.run(['docker', 'run', '-d', '--rm', '-p', f'{GRPC_PORT}:7080', IMAGE],\n", - " capture_output=True, text=True)\n", + " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", + " proc = subprocess.run(\n", + " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", + " )\n", " if proc.returncode != 0:\n", - " raise RuntimeError('docker run failed: ' + proc.stderr)\n", + " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", " container_id = proc.stdout.strip()\n", " for _ in range(30):\n", - " if _port_open(GRPC_PORT): break\n", + " if _port_open(GRPC_PORT):\n", + " break\n", " time.sleep(1)\n", " else:\n", - " subprocess.run(['docker', 'stop', container_id])\n", - " raise RuntimeError('CoordiNode did not start in 30 s')\n", - " COORDINODE_ADDR = f'localhost:{GRPC_PORT}'\n", - " print(f'CoordiNode ready at {COORDINODE_ADDR}')\n", + " subprocess.run([\"docker\", \"stop\", container_id])\n", + " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", "\n", " from coordinode import CoordinodeClient\n", + "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f'Connected to {COORDINODE_ADDR}')" + " print(f\"Connected to {COORDINODE_ADDR}\")" ] }, { @@ -169,52 +177,57 @@ "\n", "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", "\n", + "\n", "@tool\n", "def save_fact(subject: str, relation: str, obj: str) -> str:\n", " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", " client.cypher(\n", - " 'MERGE (a:Entity {name: $s, session: $sess}) '\n", - " 'MERGE (b:Entity {name: $o, session: $sess}) '\n", - " 'MERGE (a)-[r:' + relation.upper().replace(' ', '_') + ']->(b)',\n", - " params={'s': subject, 'o': obj, 'sess': SESSION}\n", + " \"MERGE (a:Entity {name: $s, session: $sess}) \"\n", + " \"MERGE (b:Entity {name: $o, session: $sess}) \"\n", + " \"MERGE (a)-[r:\" + relation.upper().replace(\" \", \"_\") + \"]->(b)\",\n", + " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", " )\n", - " return f'Saved: {subject} -[{relation}]-> {obj}'\n", + " return f\"Saved: {subject} -[{relation}]-> {obj}\"\n", + "\n", "\n", "@tool\n", "def query_facts(cypher: str) -> str:\n", " \"\"\"Run a Cypher query against the knowledge graph and return results.\n", " Use $sess to filter to the current session: WHERE n.session = $sess\"\"\"\n", - " rows = client.cypher(cypher, params={'sess': SESSION})\n", - " return str(rows[:20]) if rows else 'No results'\n", + " rows = client.cypher(cypher, params={\"sess\": SESSION})\n", + " return str(rows[:20]) if rows else \"No results\"\n", + "\n", "\n", "@tool\n", "def find_related(entity_name: str, depth: int = 1) -> str:\n", " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", " rows = client.cypher(\n", - " 'MATCH (n:Entity {name: $name, session: $sess})-[r*1..' + str(min(depth, 3)) + ']->(m) '\n", - " 'RETURN m.name AS related, type(last(r)) AS via LIMIT 20',\n", - " params={'name': entity_name, 'sess': SESSION}\n", + " \"MATCH (n:Entity {name: $name, session: $sess})-[r*1..\" + str(min(depth, 3)) + \"]->(m) \"\n", + " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", + " params={\"name\": entity_name, \"sess\": SESSION},\n", " )\n", " if not rows:\n", - " return f'No related entities found for {entity_name}'\n", - " return '\\n'.join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + " return f\"No related entities found for {entity_name}\"\n", + " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", "\n", "@tool\n", "def list_all_facts() -> str:\n", " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", " rows = client.cypher(\n", - " 'MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) '\n", - " 'RETURN a.name AS subject, type(r) AS relation, b.name AS object',\n", - " params={'sess': SESSION}\n", + " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", + " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", + " params={\"sess\": SESSION},\n", " )\n", " if not rows:\n", - " return 'No facts stored yet'\n", - " return '\\n'.join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + " return \"No facts stored yet\"\n", + " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "\n", "\n", "tools = [save_fact, query_facts, find_related, list_all_facts]\n", - "print(f'Session: {SESSION}')\n", - "print('Tools:', [t.name for t in tools])" + "print(f\"Session: {SESSION}\")\n", + "print(\"Tools:\", [t.name for t in tools])" ] }, { @@ -234,27 +247,31 @@ "metadata": {}, "outputs": [], "source": [ - "print('=== Saving facts ===')\n", - "print(save_fact.invoke({'subject': 'Alice', 'relation': 'WORKS_AT', 'obj': 'Acme Corp'}))\n", - "print(save_fact.invoke({'subject': 'Alice', 'relation': 'MANAGES', 'obj': 'Bob'}))\n", - "print(save_fact.invoke({'subject': 'Bob', 'relation': 'WORKS_AT', 'obj': 'Acme Corp'}))\n", - "print(save_fact.invoke({'subject': 'Acme Corp', 'relation': 'LOCATED_IN', 'obj': 'Berlin'}))\n", - "print(save_fact.invoke({'subject': 'Alice', 'relation': 'KNOWS', 'obj': 'Charlie'}))\n", - "print(save_fact.invoke({'subject': 'Charlie', 'relation': 'EXPERT_IN', 'obj': 'Machine Learning'}))\n", - "\n", - "print('\\n=== All facts in session ===')\n", + "print(\"=== Saving facts ===\")\n", + "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"WORKS_AT\", \"obj\": \"Acme Corp\"}))\n", + "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"MANAGES\", \"obj\": \"Bob\"}))\n", + "print(save_fact.invoke({\"subject\": \"Bob\", \"relation\": \"WORKS_AT\", \"obj\": \"Acme Corp\"}))\n", + "print(save_fact.invoke({\"subject\": \"Acme Corp\", \"relation\": \"LOCATED_IN\", \"obj\": \"Berlin\"}))\n", + "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"KNOWS\", \"obj\": \"Charlie\"}))\n", + "print(save_fact.invoke({\"subject\": \"Charlie\", \"relation\": \"EXPERT_IN\", \"obj\": \"Machine Learning\"}))\n", + "\n", + "print(\"\\n=== All facts in session ===\")\n", "print(list_all_facts.invoke({}))\n", "\n", - "print('\\n=== Related to Alice (depth=1) ===')\n", - "print(find_related.invoke({'entity_name': 'Alice', 'depth': 1}))\n", + "print(\"\\n=== Related to Alice (depth=1) ===\")\n", + "print(find_related.invoke({\"entity_name\": \"Alice\", \"depth\": 1}))\n", "\n", - "print('\\n=== Related to Alice (depth=2) ===')\n", - "print(find_related.invoke({'entity_name': 'Alice', 'depth': 2}))\n", + "print(\"\\n=== Related to Alice (depth=2) ===\")\n", + "print(find_related.invoke({\"entity_name\": \"Alice\", \"depth\": 2}))\n", "\n", - "print('\\n=== Cypher query: who works at Acme Corp? ===')\n", - "print(query_facts.invoke({\n", - " 'cypher': 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\"}) RETURN p.name AS employee'\n", - "}))" + "print(\"\\n=== Cypher query: who works at Acme Corp? ===\")\n", + "print(\n", + " query_facts.invoke(\n", + " {\n", + " \"cypher\": 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\"}) RETURN p.name AS employee'\n", + " }\n", + " )\n", + ")" ] }, { @@ -278,37 +295,42 @@ "from typing import TypedDict, Annotated\n", "import operator\n", "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[list[str], operator.add]\n", " facts_saved: int\n", "\n", + "\n", "def extract_and_save_node(state: AgentState) -> AgentState:\n", " \"\"\"Simulates entity extraction: saves a hardcoded fact.\n", " In production this node would call an LLM to extract entities from the last message.\"\"\"\n", - " result = save_fact.invoke({'subject': 'DemoSubject', 'relation': 'DEMO_REL', 'obj': 'DemoObject'})\n", - " return {'messages': [f'[extract] {result}'], 'facts_saved': state['facts_saved'] + 1}\n", + " result = save_fact.invoke({\"subject\": \"DemoSubject\", \"relation\": \"DEMO_REL\", \"obj\": \"DemoObject\"})\n", + " return {\"messages\": [f\"[extract] {result}\"], \"facts_saved\": state[\"facts_saved\"] + 1}\n", + "\n", "\n", "def query_node(state: AgentState) -> AgentState:\n", " \"\"\"Reads the graph and appends a summary to messages.\"\"\"\n", " result = list_all_facts.invoke({})\n", - " return {'messages': [f'[query] Facts: {result[:200]}'], 'facts_saved': state['facts_saved']}\n", + " return {\"messages\": [f\"[query] Facts: {result[:200]}\"], \"facts_saved\": state[\"facts_saved\"]}\n", + "\n", "\n", "def should_query(state: AgentState) -> str:\n", - " return 'query' if state['facts_saved'] >= 1 else 'extract'\n", + " return \"query\" if state[\"facts_saved\"] >= 1 else \"extract\"\n", + "\n", "\n", "builder = StateGraph(AgentState)\n", - "builder.add_node('extract', extract_and_save_node)\n", - "builder.add_node('query', query_node)\n", - "builder.set_entry_point('extract')\n", - "builder.add_conditional_edges('extract', should_query, {'query': 'query', 'extract': 'extract'})\n", - "builder.add_edge('query', END)\n", + "builder.add_node(\"extract\", extract_and_save_node)\n", + "builder.add_node(\"query\", query_node)\n", + "builder.set_entry_point(\"extract\")\n", + "builder.add_conditional_edges(\"extract\", should_query, {\"query\": \"query\", \"extract\": \"extract\"})\n", + "builder.add_edge(\"query\", END)\n", "\n", "graph_agent = builder.compile()\n", "\n", - "result = graph_agent.invoke({'messages': ['Tell me about Alice'], 'facts_saved': 0})\n", - "print('Graph agent output:')\n", - "for msg in result['messages']:\n", - " print(' ', msg)" + "result = graph_agent.invoke({\"messages\": [\"Tell me about Alice\"], \"facts_saved\": 0})\n", + "print(\"Graph agent output:\")\n", + "for msg in result[\"messages\"]:\n", + " print(\" \", msg)" ] }, { @@ -330,25 +352,28 @@ "metadata": {}, "outputs": [], "source": [ - "if not os.environ.get('OPENAI_API_KEY'):\n", - " print('OPENAI_API_KEY not set — skipping LLM agent. See section 2 for the mock demo.')\n", + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " print(\"OPENAI_API_KEY not set — skipping LLM agent. See section 2 for the mock demo.\")\n", "else:\n", " from langchain_openai import ChatOpenAI\n", " from langgraph.prebuilt import create_react_agent\n", "\n", - " llm = ChatOpenAI(model='gpt-4o-mini', temperature=0)\n", + " llm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0)\n", " agent = create_react_agent(llm, tools)\n", "\n", - " print('Agent ready. Running demo conversation...')\n", + " print(\"Agent ready. Running demo conversation...\")\n", " messages = [\n", - " {'role': 'user', 'content': 'Save these facts: Alice works at Acme Corp, Alice manages Bob, Acme Corp is in Berlin.'},\n", - " {'role': 'user', 'content': 'Who does Alice manage?'},\n", - " {'role': 'user', 'content': 'What are all the facts about Alice?'},\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": \"Save these facts: Alice works at Acme Corp, Alice manages Bob, Acme Corp is in Berlin.\",\n", + " },\n", + " {\"role\": \"user\", \"content\": \"Who does Alice manage?\"},\n", + " {\"role\": \"user\", \"content\": \"What are all the facts about Alice?\"},\n", " ]\n", " for msg in messages:\n", - " print(f'\\n>>> {msg[\"content\"]}')\n", - " result = agent.invoke({'messages': [msg]})\n", - " print('Agent:', result['messages'][-1].content)" + " print(f\"\\n>>> {msg['content']}\")\n", + " result = agent.invoke({\"messages\": [msg]})\n", + " print(\"Agent:\", result[\"messages\"][-1].content)" ] }, { @@ -366,10 +391,22 @@ "metadata": {}, "outputs": [], "source": [ - "client.cypher('MATCH (n:Entity {session: $sess}) DETACH DELETE n', params={'sess': SESSION})\n", - "print('Cleaned up session:', SESSION)\n", + "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", + "print(\"Cleaned up session:\", SESSION)\n", "client.close()" ] } - ] + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/ruff.toml b/ruff.toml index 2a07ca7..0875667 100644 --- a/ruff.toml +++ b/ruff.toml @@ -6,6 +6,10 @@ exclude = [ "coordinode/coordinode/_proto/", # Generated version files — do not lint "**/_version.py", + # Git submodule — managed separately + "coordinode-rs/", + # Jupyter auto-save artifacts — not committed + "**/.ipynb_checkpoints/", ] [lint] @@ -14,5 +18,10 @@ ignore = [ "E501", # line length handled by formatter ] +[lint.per-file-ignores] +# Jupyter notebooks: multi-import lines, out-of-order imports after subprocess +# install cells, and unsorted imports are normal in demo notebook code. +"*.ipynb" = ["E401", "E402", "I001"] + [lint.isort] known-first-party = ["coordinode", "langchain_coordinode", "llama_index_coordinode"] From 792afcc639ae28e88774c559e8567bf6014abe41 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 20:15:11 +0300 Subject: [PATCH 05/86] fix(graph): respect client ownership in close(); fix injection in notebooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - graph.py, base.py: add _owns_client flag — close() skips shutdown for externally-injected clients (client= kwarg); only closes clients it created - notebooks 01/02: _EmbeddedAdapter.close() delegates to self._lc.close() instead of being a no-op - notebook 03: validate rel_type against _REL_TYPE_RE before Cypher interpolation; clamp find_related depth to max(1, min(int(depth), 3)) --- .../01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 19 +++++++++++++------ .../langchain_coordinode/graph.py | 11 +++++++++-- .../graph_stores/coordinode/base.py | 10 +++++++++- uv.lock | 2 +- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index ebd8799..dbbb4d0 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -129,7 +129,7 @@ " return []\n", "\n", " def close(self):\n", - " pass\n", + " self._lc.close()\n", "\n", " def get_labels(self):\n", " return []\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 7e4d6cb..3ee19ba 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -130,7 +130,7 @@ " return []\n", "\n", " def close(self):\n", - " pass\n", + " self._lc.close()\n", "\n", " def get_labels(self):\n", " return []\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index b731ed0..0f8d41f 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -172,23 +172,29 @@ "metadata": {}, "outputs": [], "source": [ - "import os, uuid\n", + "import os, re, uuid\n", "from langchain_core.tools import tool\n", "\n", "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", "\n", + "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "\n", "\n", "@tool\n", "def save_fact(subject: str, relation: str, obj: str) -> str:\n", " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " rel_type = relation.upper().replace(\" \", \"_\")\n", + " # Validate rel_type before interpolating into Cypher to prevent injection.\n", + " if not _REL_TYPE_RE.fullmatch(rel_type):\n", + " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", " client.cypher(\n", - " \"MERGE (a:Entity {name: $s, session: $sess}) \"\n", - " \"MERGE (b:Entity {name: $o, session: $sess}) \"\n", - " \"MERGE (a)-[r:\" + relation.upper().replace(\" \", \"_\") + \"]->(b)\",\n", + " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", + " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", + " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", " )\n", - " return f\"Saved: {subject} -[{relation}]-> {obj}\"\n", + " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", "\n", "\n", "@tool\n", @@ -202,8 +208,9 @@ "@tool\n", "def find_related(entity_name: str, depth: int = 1) -> str:\n", " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " safe_depth = max(1, min(int(depth), 3))\n", " rows = client.cypher(\n", - " \"MATCH (n:Entity {name: $name, session: $sess})-[r*1..\" + str(min(depth, 3)) + \"]->(m) \"\n", + " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n", " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", " params={\"name\": entity_name, \"sess\": SESSION},\n", " )\n", diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 521cc1e..dd1d98a 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -51,6 +51,7 @@ def __init__( # coordinode-embedded) instead of creating a gRPC connection. The object # must expose a ``.cypher(query, params)`` method and, optionally, # ``.get_schema_text()`` and ``.vector_search()``. + self._owns_client = client is None self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) self._schema: str | None = None self._structured_schema: dict[str, Any] | None = None @@ -233,8 +234,14 @@ def similarity_search( # ── Lifecycle ───────────────────────────────────────────────────────── def close(self) -> None: - """Close the underlying gRPC connection.""" - self._client.close() + """Close the underlying gRPC connection. + + Only closes the client if it was created internally (i.e. ``client`` was + not passed to ``__init__``). Externally-injected clients are owned by + the caller and must be closed by them. + """ + if self._owns_client: + self._client.close() def __enter__(self) -> CoordinodeGraph: return self diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index be545a9..cd9820b 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -72,6 +72,7 @@ def __init__( ) -> None: # ``client`` allows passing a pre-built client (e.g. LocalClient from # coordinode-embedded) instead of creating a gRPC connection. + self._owns_client = client is None self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) # ── Node operations ─────────────────────────────────────────────────── @@ -303,7 +304,14 @@ def get_schema_str(self, refresh: bool = False) -> str: return self.get_schema(refresh=refresh) def close(self) -> None: - self._client.close() + """Close the underlying gRPC connection. + + Only closes the client if it was created internally (i.e. ``client`` was + not passed to ``__init__``). Externally-injected clients are owned by + the caller and must be closed by them. + """ + if self._owns_client: + self._client.close() def __enter__(self) -> CoordinodePropertyGraphStore: return self diff --git a/uv.lock b/uv.lock index 72a32e3..e0cce2b 100644 --- a/uv.lock +++ b/uv.lock @@ -359,7 +359,7 @@ provides-extras = ["dev"] [[package]] name = "coordinode-workspace" -version = "0.6.0" +version = "0.7.0" source = { virtual = "." } dependencies = [ { name = "googleapis-common-protos" }, From 6710ac605b5f56200cd32beffba83b478901128e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 20:16:17 +0300 Subject: [PATCH 06/86] docs(notebooks): document stub methods in _EmbeddedAdapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vector_search(), get_labels(), get_edge_types() return empty results in embedded mode — add inline comments explaining this limitation so users understand the behavior without reading server-side code --- demo/notebooks/01_llama_index_property_graph.ipynb | 3 +++ demo/notebooks/02_langchain_graph_chain.ipynb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index dbbb4d0..d5951e5 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -126,15 +126,18 @@ " return \"\\n\".join(lines)\n", "\n", " def vector_search(self, **kwargs):\n", + " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n", " return []\n", "\n", " def close(self):\n", " self._lc.close()\n", "\n", " def get_labels(self):\n", + " # Schema introspection not available in embedded mode.\n\n", " return []\n", "\n", " def get_edge_types(self):\n", + " # Schema introspection not available in embedded mode.\n", " return []" ] }, diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 3ee19ba..c927d46 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -127,15 +127,18 @@ " return \"\\n\".join(lines)\n", "\n", " def vector_search(self, **kwargs):\n", + " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n", " return []\n", "\n", " def close(self):\n", " self._lc.close()\n", "\n", " def get_labels(self):\n", + " # Schema introspection not available in embedded mode.\n\n", " return []\n", "\n", " def get_edge_types(self):\n", + " # Schema introspection not available in embedded mode.\n", " return []" ] }, From 0464697025319f4f265a9ca42cf882791cf3ca84 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:06:37 +0300 Subject: [PATCH 07/86] fix(notebooks): close injected client explicitly; guard query_facts; fix CONTAINER_ID scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - notebooks 01/02: add explicit client.close() after store/graph.close() in cleanup cells — _owns_client prevents store from closing injected clients - notebook 03: promote container_id to module-level CONTAINER_ID = None so cleanup cell can stop auto-started Docker containers on re-runs - notebook 03: add write-keyword guard and $sess requirement to query_facts to prevent destructive or cross-session Cypher from agent-facing tool - graph.py:82: replace TODO comment with descriptive note referencing G010/GAPS.md to silence SonarCloud warning --- .../01_llama_index_property_graph.ipynb | 9 ++++--- demo/notebooks/02_langchain_graph_chain.ipynb | 9 ++++--- demo/notebooks/03_langgraph_agent.ipynb | 27 ++++++++++++++----- .../langchain_coordinode/graph.py | 3 ++- 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index d5951e5..d04164f 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -126,14 +126,16 @@ " return \"\\n\".join(lines)\n", "\n", " def vector_search(self, **kwargs):\n", - " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n", + " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n", + "\n", " return []\n", "\n", " def close(self):\n", " self._lc.close()\n", "\n", " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n\n", + " # Schema introspection not available in embedded mode.\n", + "\n", " return []\n", "\n", " def get_edge_types(self):\n", @@ -395,7 +397,8 @@ "source": [ "store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n", "print(\"Cleaned up\")\n", - "store.close()" + "store.close()\n", + "client.close() # injected client — owned by callerclient.close() # injected client — owned by caller" ] } ], diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index c927d46..fbbdbf0 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -127,14 +127,16 @@ " return \"\\n\".join(lines)\n", "\n", " def vector_search(self, **kwargs):\n", - " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n", + " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n", + "\n", " return []\n", "\n", " def close(self):\n", " self._lc.close()\n", "\n", " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n\n", + " # Schema introspection not available in embedded mode.\n", + "\n", " return []\n", "\n", " def get_edge_types(self):\n", @@ -393,7 +395,8 @@ "source": [ "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\n", "print(\"Cleaned up\")\n", - "graph.close()" + "graph.close()\n", + "client.close() # injected client — owned by callerclient.close() # injected client — owned by caller" ] } ], diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 0f8d41f..a37c4f1 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -108,6 +108,10 @@ "source": [ "import os, socket, subprocess, time\n", "\n", + "# CONTAINER_ID tracks a Docker container started by this notebook so the\n", + "# cleanup cell can stop it. None means no container was auto-started.\n", + "CONTAINER_ID = None\n", + "\n", "if IN_COLAB:\n", " from coordinode_embedded import LocalClient\n", "\n", @@ -137,13 +141,13 @@ " )\n", " if proc.returncode != 0:\n", " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", - " container_id = proc.stdout.strip()\n", + " CONTAINER_ID = proc.stdout.strip()\n", " for _ in range(30):\n", " if _port_open(GRPC_PORT):\n", " break\n", " time.sleep(1)\n", " else:\n", - " subprocess.run([\"docker\", \"stop\", container_id])\n", + " subprocess.run([\"docker\", \"stop\", CONTAINER_ID])\n", " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", @@ -178,6 +182,8 @@ "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", "\n", "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "# Write keywords that query_facts must not execute (demo safety guard).\n", + "_WRITE_KEYWORDS = (\"CREATE \", \"MERGE \", \"DELETE \", \"DETACH \", \"SET \", \"REMOVE \", \"DROP \")\n", "\n", "\n", "@tool\n", @@ -199,9 +205,15 @@ "\n", "@tool\n", "def query_facts(cypher: str) -> str:\n", - " \"\"\"Run a Cypher query against the knowledge graph and return results.\n", - " Use $sess to filter to the current session: WHERE n.session = $sess\"\"\"\n", - " rows = client.cypher(cypher, params={\"sess\": SESSION})\n", + " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", + " Must include $sess filtering: WHERE n.session = $sess\"\"\"\n", + " q = cypher.strip()\n", + " upper_q = q.upper()\n", + " if any(kw in upper_q for kw in _WRITE_KEYWORDS):\n", + " return \"Only read-only Cypher is allowed in query_facts.\"\n", + " if \"$sess\" not in q:\n", + " return \"Query must include $sess to restrict results to the current session.\"\n", + " rows = client.cypher(q, params={\"sess\": SESSION})\n", " return str(rows[:20]) if rows else \"No results\"\n", "\n", "\n", @@ -400,7 +412,10 @@ "source": [ "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", "print(\"Cleaned up session:\", SESSION)\n", - "client.close()" + "client.close()\n", + "if CONTAINER_ID:\n", + " subprocess.run([\"docker\", \"stop\", CONTAINER_ID], check=False)\n", + " print(f\"Stopped container {CONTAINER_ID}\")" ] } ], diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index dd1d98a..294d91e 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -79,7 +79,8 @@ def refresh_schema(self) -> None: structured = _parse_schema(text) # Augment with relationship triples (start_label, type, end_label). # No LIMIT: RETURN DISTINCT bounds result by unique triples, not edge count. - # TODO: simplify to labels(a)[0] once subscript-on-function is in published image. + # Note: can simplify to labels(a)[0] once subscript-on-function support lands in the + # published Docker image (tracked in G010 / GAPS.md). rows = self._client.cypher( "MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels" ) From 6b69045b060e1d272ed620da2db897b3c7d46f32 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:09:36 +0300 Subject: [PATCH 08/86] fix(client): validate property name in _build_property_definitions; sync docstring types - _build_property_definitions: raise ValueError with index when property entry is not a dict with a non-empty 'name' key (was KeyError) - LabelInfo.schema_mode inline annotation: 0=unspecified (not 0=unspecified/strict) - create_label docstring: add "list" and "map" to the type strings list - _first_label docstring: clarify min() is intentional for determinism; labels(n)[0] is unstable due to non-guaranteed label ordering --- coordinode/coordinode/client.py | 12 +++++++----- langchain-coordinode/langchain_coordinode/graph.py | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index daf8b6d..b1e48f7 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -105,7 +105,7 @@ def __init__(self, proto_label: Any) -> None: self.name: str = proto_label.name self.version: int = proto_label.version self.properties: list[PropertyDefinitionInfo] = [PropertyDefinitionInfo(p) for p in proto_label.properties] - # schema_mode: 0=unspecified/strict, 1=strict, 2=validated, 3=flexible + # schema_mode: 0=unspecified, 1=strict, 2=validated, 3=flexible self.schema_mode: int = getattr(proto_label, "schema_mode", 0) def __repr__(self) -> str: @@ -391,12 +391,14 @@ def _build_property_definitions( "map": PropertyType.PROPERTY_TYPE_MAP, } result = [] - for p in properties or []: + for idx, p in enumerate(properties or []): + name = p.get("name") if isinstance(p, dict) else None + if not isinstance(name, str) or not name: + raise ValueError(f"Property at index {idx} must be a dict with a non-empty 'name' key; got {p!r}") type_str = str(p.get("type", "string")).lower() if type_str not in type_map: raise ValueError( - f"Unknown property type {type_str!r} for property {p['name']!r}. " - f"Expected one of: {sorted(type_map)}" + f"Unknown property type {type_str!r} for property {name!r}. Expected one of: {sorted(type_map)}" ) result.append( PropertyDefinition( @@ -423,7 +425,7 @@ async def create_label( ``name`` (str), ``type`` (str), ``required`` (bool), ``unique`` (bool). Type strings: ``"string"``, ``"int64"``, ``"float64"``, ``"bool"``, ``"bytes"``, - ``"timestamp"``, ``"vector"``. + ``"timestamp"``, ``"vector"``, ``"list"``, ``"map"``. schema_mode: ``"strict"`` (default — reject undeclared props), ``"validated"`` (allow extra props without interning), ``"flexible"`` (no enforcement). diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 294d91e..eaf7014 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -285,8 +285,10 @@ def _first_label(labels: Any) -> str | None: possible. ``min()`` gives a deterministic result regardless of how many labels are present. - TODO: replace with ``labels(n)[0]`` in Cypher once subscript-on-function - lands in the published Docker image. + Note: once subscript-on-function support lands in the published Docker + image (tracked in G010 / GAPS.md), this Python helper could be replaced + by an inline Cypher expression — but keep the deterministic ``min()`` + rule rather than index 0, since label ordering is not guaranteed stable. """ if isinstance(labels, list) and labels: return str(min(labels)) From e1252e36a7ca51f6aa73c5dbd14ce2e718a03023 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:15:53 +0300 Subject: [PATCH 09/86] test(integration): declare tag property in strict label for create_label visibility test Server now enforces strict schema for labels registered via create_label(). The workaround node must include only declared properties, so add tag:string to the schema alongside x:int64. --- tests/integration/test_sdk.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 5eeb48e..c8395c7 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -453,7 +453,15 @@ def test_create_label_appears_in_get_labels(client): """ name = f"CreateLabelVisible{uid()}" tag = uid() - client.create_label(name, properties=[{"name": "x", "type": "int64"}]) + # Declare both properties used in the workaround node so the strict label + # does not reject the CREATE (server now enforces strict schema). + client.create_label( + name, + properties=[ + {"name": "x", "type": "int64"}, + {"name": "tag", "type": "string"}, + ], + ) # Workaround: create a node so the label appears in ListLabels. client.cypher(f"CREATE (n:{name} {{x: 1, tag: $tag}})", params={"tag": tag}) try: From fcf1166001360d000c43c4da30907ecd2891283a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:17:14 +0300 Subject: [PATCH 10/86] refactor(client): rename _build_property_definitions params to snake_case; fix duplicate client.close() in notebooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _build_property_definitions: PropertyType → property_type_cls, PropertyDefinition → property_definition_cls (SonarCloud naming rule) - notebooks 01/02: remove duplicated client.close() line in cleanup cells --- coordinode/coordinode/client.py | 24 +++++++++---------- .../01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index b1e48f7..008a78e 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -371,8 +371,8 @@ async def get_edge_types(self) -> list[EdgeTypeInfo]: @staticmethod def _build_property_definitions( properties: list[dict[str, Any]] | None, - PropertyType: Any, - PropertyDefinition: Any, + property_type_cls: Any, + property_definition_cls: Any, ) -> list[Any]: """Convert property dicts to proto PropertyDefinition objects. @@ -380,15 +380,15 @@ def _build_property_definitions( duplicating the type-map and validation logic. """ type_map = { - "int64": PropertyType.PROPERTY_TYPE_INT64, - "float64": PropertyType.PROPERTY_TYPE_FLOAT64, - "string": PropertyType.PROPERTY_TYPE_STRING, - "bool": PropertyType.PROPERTY_TYPE_BOOL, - "bytes": PropertyType.PROPERTY_TYPE_BYTES, - "timestamp": PropertyType.PROPERTY_TYPE_TIMESTAMP, - "vector": PropertyType.PROPERTY_TYPE_VECTOR, - "list": PropertyType.PROPERTY_TYPE_LIST, - "map": PropertyType.PROPERTY_TYPE_MAP, + "int64": property_type_cls.PROPERTY_TYPE_INT64, + "float64": property_type_cls.PROPERTY_TYPE_FLOAT64, + "string": property_type_cls.PROPERTY_TYPE_STRING, + "bool": property_type_cls.PROPERTY_TYPE_BOOL, + "bytes": property_type_cls.PROPERTY_TYPE_BYTES, + "timestamp": property_type_cls.PROPERTY_TYPE_TIMESTAMP, + "vector": property_type_cls.PROPERTY_TYPE_VECTOR, + "list": property_type_cls.PROPERTY_TYPE_LIST, + "map": property_type_cls.PROPERTY_TYPE_MAP, } result = [] for idx, p in enumerate(properties or []): @@ -401,7 +401,7 @@ def _build_property_definitions( f"Unknown property type {type_str!r} for property {name!r}. Expected one of: {sorted(type_map)}" ) result.append( - PropertyDefinition( + property_definition_cls( name=p["name"], type=type_map[type_str], required=bool(p.get("required", False)), diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index d04164f..efb371c 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -398,7 +398,7 @@ "store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n", "print(\"Cleaned up\")\n", "store.close()\n", - "client.close() # injected client — owned by callerclient.close() # injected client — owned by caller" + "client.close() # injected client — owned by caller" ] } ], diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index fbbdbf0..2a63b4d 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -396,7 +396,7 @@ "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\n", "print(\"Cleaned up\")\n", "graph.close()\n", - "client.close() # injected client — owned by callerclient.close() # injected client — owned by caller" + "client.close() # injected client — owned by caller" ] } ], From 54477491b2066a4c57faeef606f0e6b87bdade5f Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:42:24 +0300 Subject: [PATCH 11/86] feat(adapters): use get_labels/get_edge_types in refresh_schema; bump dep to >=0.6.0 - CoordinodeGraph.refresh_schema() now uses get_labels() + get_edge_types() when available (coordinode>=0.6.0), bypassing the regex text parser; injected clients without these methods fall back to _parse_schema() - Add _PROPERTY_TYPE_NAME dict mapping PropertyType proto ints to strings (INT64, FLOAT64, STRING, BOOL, BYTES, TIMESTAMP, VECTOR, LIST, MAP) - Bump coordinode requirement to >=0.6.0 in both langchain-coordinode and llama-index-graph-stores-coordinode --- .../langchain_coordinode/graph.py | 46 +++++++++++++++++-- langchain-coordinode/pyproject.toml | 2 +- llama-index-coordinode/pyproject.toml | 2 +- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index eaf7014..3b63d04 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -73,10 +73,31 @@ def structured_schema(self) -> dict[str, Any]: return self._structured_schema or {} def refresh_schema(self) -> None: - """Fetch current schema from CoordiNode.""" - text = self._client.get_schema_text() - self._schema = text - structured = _parse_schema(text) + """Fetch current schema from CoordiNode. + + Prefers the structured ``get_labels()`` / ``get_edge_types()`` API + (available since ``coordinode`` 0.6.0) over the legacy text-parsing + path. Injected clients (e.g. ``_EmbeddedAdapter`` in Colab notebooks) + that do not expose those methods fall back to ``_parse_schema()``. + """ + self._schema = self._client.get_schema_text() + + if hasattr(self._client, "get_labels") and hasattr(self._client, "get_edge_types"): + node_props: dict[str, list[dict[str, str]]] = {} + for label in self._client.get_labels(): + node_props[label.name] = [ + {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} + for p in label.properties + ] + rel_props: dict[str, list[dict[str, str]]] = {} + for et in self._client.get_edge_types(): + rel_props[et.name] = [ + {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} for p in et.properties + ] + structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} + else: + structured = _parse_schema(self._schema) + # Augment with relationship triples (start_label, type, end_label). # No LIMIT: RETURN DISTINCT bounds result by unique triples, not edge count. # Note: can simplify to labels(a)[0] once subscript-on-function support lands in the @@ -251,7 +272,22 @@ def __exit__(self, *args: Any) -> None: self.close() -# ── Schema parser ───────────────────────────────────────────────────────── +# ── Schema helpers ──────────────────────────────────────────────────────── + +# Maps PropertyType protobuf enum integers to LangChain-compatible type strings. +# Values mirror coordinode.v1.graph.PropertyType (schema.proto). +_PROPERTY_TYPE_NAME: dict[int, str] = { + 0: "UNSPECIFIED", + 1: "INT64", + 2: "FLOAT64", + 3: "STRING", + 4: "BOOL", + 5: "BYTES", + 6: "TIMESTAMP", + 7: "VECTOR", + 8: "LIST", + 9: "MAP", +} def _stable_document_id(source: Any) -> str: diff --git a/langchain-coordinode/pyproject.toml b/langchain-coordinode/pyproject.toml index 443a3f2..4946407 100644 --- a/langchain-coordinode/pyproject.toml +++ b/langchain-coordinode/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ - "coordinode>=0.4.0", + "coordinode>=0.6.0", "langchain-community>=0.2.0", ] diff --git a/llama-index-coordinode/pyproject.toml b/llama-index-coordinode/pyproject.toml index 62e2963..92069a9 100644 --- a/llama-index-coordinode/pyproject.toml +++ b/llama-index-coordinode/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ - "coordinode>=0.4.0", + "coordinode>=0.6.0", "llama-index-core>=0.10.0", ] From d1d5e80cc3b4cb17f0cd3c2d383c7dc5a924aa70 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:50:28 +0300 Subject: [PATCH 12/86] feat(fts): add text_search() and hybrid_text_vector_search() via TextService - Update proto submodule and coordinode-rs to main (includes text.proto with TextService: TextSearch + HybridTextVectorSearch RRF methods) - Regenerate _proto stubs: text_pb2.py / text_pb2_grpc.py / text_pb2.pyi - Add TextResult (node_id, score, snippet) and HybridResult (node_id, score) - Add AsyncCoordinodeClient.text_search() / hybrid_text_vector_search() with full docstrings (BM25, fuzzy, language, RRF weights) - Add CoordinodeClient sync wrappers - Export TextResult and HybridResult from coordinode.__init__ - Add integration tests (xfail against servers <0.3.8 without FTS) --- coordinode-rs | 2 +- coordinode/coordinode/__init__.py | 4 + coordinode/coordinode/client.py | 149 ++++++++++++++++++++++++++++++ proto | 2 +- tests/integration/test_sdk.py | 95 ++++++++++++++++++- 5 files changed, 249 insertions(+), 3 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index 0aa5bfa..b4b8fa6 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 0aa5bfa52214033f757ac2b7ea3be9e7d6798c3c +Subproject commit b4b8fa61ee842e302300f90917b00311bd942d7a diff --git a/coordinode/coordinode/__init__.py b/coordinode/coordinode/__init__.py index d36407f..ddb3482 100644 --- a/coordinode/coordinode/__init__.py +++ b/coordinode/coordinode/__init__.py @@ -23,9 +23,11 @@ CoordinodeClient, EdgeResult, EdgeTypeInfo, + HybridResult, LabelInfo, NodeResult, PropertyDefinitionInfo, + TextResult, TraverseResult, VectorResult, ) @@ -40,6 +42,8 @@ "NodeResult", "EdgeResult", "VectorResult", + "TextResult", + "HybridResult", "LabelInfo", "EdgeTypeInfo", "PropertyDefinitionInfo", diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 008a78e..9c11f44 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -85,6 +85,31 @@ def __repr__(self) -> str: return f"VectorResult(distance={self.distance:.4f}, node={self.node})" +class TextResult: + """A single full-text search result with BM25 score and optional snippet.""" + + def __init__(self, proto_result: Any) -> None: + self.node_id: int = proto_result.node_id + self.score: float = proto_result.score + # HTML snippet with highlights. Empty when unavailable. + self.snippet: str = proto_result.snippet + + def __repr__(self) -> str: + return f"TextResult(node_id={self.node_id}, score={self.score:.4f}, snippet={self.snippet!r})" + + +class HybridResult: + """A single result from hybrid text + vector search (RRF-ranked).""" + + def __init__(self, proto_result: Any) -> None: + self.node_id: int = proto_result.node_id + # Combined RRF score: text_weight/(60+rank_text) + vector_weight/(60+rank_vec). + self.score: float = proto_result.score + + def __repr__(self) -> str: + return f"HybridResult(node_id={self.node_id}, score={self.score:.6f})" + + class PropertyDefinitionInfo: """A property definition from the schema (name, type, required, unique).""" @@ -194,6 +219,7 @@ async def connect(self) -> None: self._channel = _make_async_channel(self._host, self._port, self._tls) self._cypher_stub = _cypher_stub(self._channel) self._vector_stub = _vector_stub(self._channel) + self._text_stub = _text_stub(self._channel) self._graph_stub = _graph_stub(self._channel) self._schema_stub = _schema_stub(self._channel) self._health_stub = _health_stub(self._channel) @@ -530,6 +556,87 @@ async def traverse( resp = await self._graph_stub.Traverse(req, timeout=self._timeout) return TraverseResult(resp) + async def text_search( + self, + label: str, + query: str, + *, + limit: int = 10, + fuzzy: bool = False, + language: str = "", + ) -> list[TextResult]: + """Run a full-text BM25 search over all indexed text properties for *label*. + + Args: + label: Node label to search (e.g. ``"Article"``). Must have at least + one text index registered; returns ``[]`` otherwise. + query: Full-text query string. Supports boolean operators (``AND``, + ``OR``, ``NOT``), phrase search (``"exact phrase"``), prefix + wildcards (``term*``), and per-term boosting (``term^N``). + limit: Maximum results to return (default 10, capped at 1000). + fuzzy: If ``True``, apply Levenshtein-1 fuzzy matching to individual + terms. Increases recall at the cost of precision. + language: Tokenization/stemming language (e.g. ``"english"``, + ``"russian"``). Empty string uses the index's default language. + + Returns: + List of :class:`TextResult` ordered by BM25 score descending. + """ + from coordinode._proto.coordinode.v1.query.text_pb2 import TextSearchRequest # type: ignore[import] + + req = TextSearchRequest(label=label, query=query, limit=limit, fuzzy=fuzzy, language=language) + resp = await self._text_stub.TextSearch(req, timeout=self._timeout) + return [TextResult(r) for r in resp.results] + + async def hybrid_text_vector_search( + self, + label: str, + text_query: str, + vector: Sequence[float], + *, + limit: int = 10, + text_weight: float = 0.5, + vector_weight: float = 0.5, + vector_property: str = "embedding", + ) -> list[HybridResult]: + """Fuse BM25 text search and cosine vector search using Reciprocal Rank Fusion (RRF). + + Runs text and vector searches independently, then combines their ranked + lists:: + + rrf_score(node) = text_weight / (60 + rank_text) + + vector_weight / (60 + rank_vec) + + Args: + label: Node label to search (e.g. ``"Article"``). + text_query: Full-text query string (same syntax as :meth:`text_search`). + vector: Query embedding vector. Must match the dimensionality stored + in *vector_property*. + limit: Maximum fused results to return (default 10, capped at 1000). + text_weight: Weight for the BM25 component (default 0.5). + vector_weight: Weight for the cosine component (default 0.5). + vector_property: Node property containing the embedding (default + ``"embedding"``). + + Returns: + List of :class:`HybridResult` ordered by RRF score descending. + """ + from coordinode._proto.coordinode.v1.query.text_pb2 import ( # type: ignore[import] + HybridTextVectorSearchRequest, + ) + + req = HybridTextVectorSearchRequest( + label=label, + text_query=text_query, + vector=list(vector), + limit=limit, + text_weight=text_weight, + vector_weight=vector_weight, + vector_property=vector_property, + ) + resp = await self._text_stub.HybridTextVectorSearch(req, timeout=self._timeout) + return [HybridResult(r) for r in resp.results] + async def health(self) -> bool: from coordinode._proto.coordinode.v1.health.health_pb2 import ( # type: ignore[import] HealthCheckRequest, @@ -685,6 +792,42 @@ def traverse( """Traverse the graph from *start_node_id* following *edge_type* edges.""" return self._run(self._async.traverse(start_node_id, edge_type, direction, max_depth)) + def text_search( + self, + label: str, + query: str, + *, + limit: int = 10, + fuzzy: bool = False, + language: str = "", + ) -> list[TextResult]: + """Run a full-text BM25 search over all indexed text properties for *label*.""" + return self._run(self._async.text_search(label, query, limit=limit, fuzzy=fuzzy, language=language)) + + def hybrid_text_vector_search( + self, + label: str, + text_query: str, + vector: Sequence[float], + *, + limit: int = 10, + text_weight: float = 0.5, + vector_weight: float = 0.5, + vector_property: str = "embedding", + ) -> list[HybridResult]: + """Fuse BM25 text search and cosine vector search using RRF ranking.""" + return self._run( + self._async.hybrid_text_vector_search( + label, + text_query, + vector, + limit=limit, + text_weight=text_weight, + vector_weight=vector_weight, + vector_property=vector_property, + ) + ) + def health(self) -> bool: return self._run(self._async.health()) @@ -704,6 +847,12 @@ def _vector_stub(channel: Any) -> Any: return VectorServiceStub(channel) +def _text_stub(channel: Any) -> Any: + from coordinode._proto.coordinode.v1.query.text_pb2_grpc import TextServiceStub # type: ignore[import] + + return TextServiceStub(channel) + + def _graph_stub(channel: Any) -> Any: from coordinode._proto.coordinode.v1.graph.graph_pb2_grpc import GraphServiceStub # type: ignore[import] diff --git a/proto b/proto index 381dbcd..97c11c6 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 381dbcd72677112a393d485d8992eda1665d228c +Subproject commit 97c11c6951bb47a5170b2f0580f892c738fcb63f diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index c8395c7..d5043ef 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -12,7 +12,15 @@ import pytest -from coordinode import AsyncCoordinodeClient, CoordinodeClient, EdgeTypeInfo, LabelInfo, TraverseResult +from coordinode import ( + AsyncCoordinodeClient, + CoordinodeClient, + EdgeTypeInfo, + HybridResult, + LabelInfo, + TextResult, + TraverseResult, +) ADDR = os.environ.get("COORDINODE_ADDR", "localhost:7080") @@ -536,3 +544,88 @@ def test_vector_search_returns_results(client): assert hasattr(results[0], "node") finally: client.cypher("MATCH (n:VecSDKTest {tag: $tag}) DELETE n", params={"tag": tag}) + + +# ── Full-text search ────────────────────────────────────────────────────────── + +# FTS tests require a CoordiNode server with TextService implemented (>=0.3.8). +# They are marked xfail so the suite stays green against older servers; once +# upgraded, the tests turn into expected passes automatically. +_fts = pytest.mark.xfail( + reason="TextService requires CoordiNode >=0.3.8 with FTS support", + strict=False, +) + + +@_fts +def test_text_search_returns_results(client): + """text_search() finds nodes whose text property matches the query.""" + tag = uid() + client.cypher( + "CREATE (n:FtsTest {tag: $tag, body: 'machine learning and neural networks'})", + params={"tag": tag}, + ) + try: + results = client.text_search("FtsTest", "machine learning", limit=5) + assert isinstance(results, list) + assert len(results) >= 1, "text_search returned no results" + r = results[0] + assert isinstance(r, TextResult) + assert isinstance(r.node_id, int) + assert isinstance(r.score, float) + assert r.score > 0 + assert isinstance(r.snippet, str) + finally: + client.cypher("MATCH (n:FtsTest {tag: $tag}) DELETE n", params={"tag": tag}) + + +@_fts +def test_text_search_empty_for_unindexed_label(client): + """text_search() returns [] for a label with no text index (no error).""" + results = client.text_search("NoSuchLabelForFts_" + uid(), "anything") + assert results == [] + + +@_fts +def test_text_search_fuzzy(client): + """text_search() with fuzzy=True matches approximate terms.""" + tag = uid() + client.cypher( + "CREATE (n:FtsFuzzyTest {tag: $tag, body: 'coordinode graph database'})", + params={"tag": tag}, + ) + try: + # "coordinode" with a typo — fuzzy should still match + results = client.text_search("FtsFuzzyTest", "coordinod", fuzzy=True, limit=5) + assert isinstance(results, list) + # May return 0 results if fuzzy is not yet supported or index is cold; + # just verify the call does not raise. + finally: + client.cypher("MATCH (n:FtsFuzzyTest {tag: $tag}) DELETE n", params={"tag": tag}) + + +@_fts +def test_hybrid_text_vector_search_returns_results(client): + """hybrid_text_vector_search() returns HybridResult list with RRF scores.""" + tag = uid() + vec = [float(i) / 16 for i in range(16)] + client.cypher( + "CREATE (n:FtsHybridTest {tag: $tag, body: 'graph neural network embedding', embedding: $vec})", + params={"tag": tag, "vec": vec}, + ) + try: + results = client.hybrid_text_vector_search( + "FtsHybridTest", + "graph neural", + vec, + limit=5, + ) + assert isinstance(results, list) + assert len(results) >= 1, "hybrid_text_vector_search returned no results" + r = results[0] + assert isinstance(r, HybridResult) + assert isinstance(r.node_id, int) + assert isinstance(r.score, float) + assert r.score > 0 + finally: + client.cypher("MATCH (n:FtsHybridTest {tag: $tag}) DETACH DELETE n", params={"tag": tag}) From f179b2e5e412fe4d2792c3a00d241a32300374a1 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 21:53:14 +0300 Subject: [PATCH 13/86] build(deps): bump coordinode-rs submodule to v0.3.9 Pins the coordinode-rs submodule to the tagged release v0.3.9 (fetched from github.com/structured-world/coordinode). This is the first release to include TextService (full-text search). Proto stubs regenerated from the updated coordinode-proto-ce submodule which now includes text.proto. --- coordinode-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinode-rs b/coordinode-rs index b4b8fa6..f6558cc 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit b4b8fa61ee842e302300f90917b00311bd942d7a +Subproject commit f6558ccf19322cd86815ef303bd2bd337bd9fc42 From 1b200937444a21c0169c49bfd9f6dabe29cec2e3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 22:13:26 +0300 Subject: [PATCH 14/86] fix(client): normalize schema_mode, validate bool flags, strengthen injected client guards - schema_mode: apply .strip().lower() before lookup so "STRICT"/"Strict" work - _build_property_definitions: check isinstance(p, dict) before any .get() call - _build_property_definitions: reject non-bool required/unique (bool("false") == True) - CoordinodeGraph.refresh_schema: guard get_schema_text() with hasattr for bare LocalClient - CoordinodeGraph.similarity_search: guard vector_search() with hasattr for bare LocalClient - llama-index adapter get_schema/vector_query: same hasattr guards - close() docstrings in both adapters: "gRPC" -> transport-agnostic wording - .gitignore: use **/.ipynb_checkpoints/ (recursive, not root-only) - test comment: remove inaccurate claim about strict schema enforcement being active - notebooks: replace Docker auto-start with embedded-engine fallback in all 4 notebooks; install coordinode-embedded in both Colab and local paths - notebooks: use full tag in STARTS WITH queries (not tag[:4]) - notebook 03: strengthen query_facts to require WHERE .session = $sess - tests/unit: add validation tests for non-dict, non-bool, schema_mode normalization --- .gitignore | 2 +- coordinode/coordinode/client.py | 24 +++- demo/notebooks/00_seed_data.ipynb | 108 ++++++-------- .../01_llama_index_property_graph.ipynb | 124 +++++++--------- demo/notebooks/02_langchain_graph_chain.ipynb | 127 ++++++----------- demo/notebooks/03_langgraph_agent.ipynb | 132 ++++++------------ .../langchain_coordinode/graph.py | 16 ++- .../graph_stores/coordinode/base.py | 21 ++- tests/integration/test_sdk.py | 5 +- tests/unit/test_schema_crud.py | 97 +++++++++++++ 10 files changed, 329 insertions(+), 327 deletions(-) diff --git a/.gitignore b/.gitignore index 1800bea..efc30ce 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ langchain-coordinode/langchain_coordinode/_version.py llama-index-coordinode/llama_index/graph_stores/coordinode/_version.py GAPS.md CLAUDE.md -.ipynb_checkpoints/ +**/.ipynb_checkpoints/ diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 9c11f44..6a984d6 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -418,20 +418,29 @@ def _build_property_definitions( } result = [] for idx, p in enumerate(properties or []): - name = p.get("name") if isinstance(p, dict) else None + if not isinstance(p, dict): + raise ValueError(f"Property at index {idx} must be a dict; got {p!r}") + name = p.get("name") if not isinstance(name, str) or not name: - raise ValueError(f"Property at index {idx} must be a dict with a non-empty 'name' key; got {p!r}") + raise ValueError(f"Property at index {idx} must have a non-empty 'name' key; got {p!r}") type_str = str(p.get("type", "string")).lower() if type_str not in type_map: raise ValueError( f"Unknown property type {type_str!r} for property {name!r}. Expected one of: {sorted(type_map)}" ) + required = p.get("required", False) + unique = p.get("unique", False) + if not isinstance(required, bool) or not isinstance(unique, bool): + raise ValueError( + f"Property {name!r} must use boolean values for 'required' and 'unique'; got " + f"required={required!r}, unique={unique!r}" + ) result.append( property_definition_cls( - name=p["name"], + name=name, type=type_map[type_str], - required=bool(p.get("required", False)), - unique=bool(p.get("unique", False)), + required=required, + unique=unique, ) ) return result @@ -468,14 +477,15 @@ async def create_label( "validated": SchemaMode.SCHEMA_MODE_VALIDATED, "flexible": SchemaMode.SCHEMA_MODE_FLEXIBLE, } - if schema_mode not in _mode_map: + schema_mode_normalized = schema_mode.strip().lower() + if schema_mode_normalized not in _mode_map: raise ValueError(f"schema_mode must be one of {list(_mode_map)}, got {schema_mode!r}") proto_props = self._build_property_definitions(properties, PropertyType, PropertyDefinition) req = CreateLabelRequest( name=name, properties=proto_props, - schema_mode=_mode_map[schema_mode], + schema_mode=_mode_map[schema_mode_normalized], ) label = await self._schema_stub.CreateLabel(req, timeout=self._timeout) return LabelInfo(label) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 7b561e9..aa9877e 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -45,36 +45,24 @@ "\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", - "else:\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + ")\n", "\n", "import nest_asyncio\n", "\n", @@ -91,7 +79,8 @@ "## Connect to CoordiNode\n", "\n", "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", - "- **Local**: checks for a running server on port 7080, or starts one via Docker." + "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", + "- **Local without server**: falls back to embedded engine automatically." ] }, { @@ -101,52 +90,37 @@ "metadata": {}, "outputs": [], "source": [ - "import os, socket, subprocess, time\n", + "import os, socket\n", "\n", - "if IN_COLAB:\n", - " from coordinode_embedded import LocalClient\n", "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")\n", - "else:\n", - " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", - "\n", - " def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - " if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", - " elif _port_open(GRPC_PORT):\n", - " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " else:\n", - " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", - " proc = subprocess.run(\n", - " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", - " )\n", - " if proc.returncode != 0:\n", - " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", - " container_id = proc.stdout.strip()\n", - " for _ in range(30):\n", - " if _port_open(GRPC_PORT):\n", - " break\n", - " time.sleep(1)\n", - " else:\n", - " subprocess.run([\"docker\", \"stop\", container_id])\n", - " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", + "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")" + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " from coordinode_embedded import LocalClient\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index efb371c..2c38f46 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -44,40 +44,28 @@ "\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", + "# In Colab, build from source (~5 min first run, cached after).\n", + "# Locally, it installs in seconds if a pre-built wheel is cached.\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"llama-index-graph-stores-coordinode\",\n", - " \"llama-index-core\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", - "else:\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"llama-index-graph-stores-coordinode\",\n", - " \"llama-index-core\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + ")\n", "\n", "import nest_asyncio\n", "\n", @@ -93,9 +81,9 @@ "source": [ "## Adapter for embedded mode\n", "\n", - "In Colab we use `LocalClient` (embedded engine) which has the same `.cypher()` API as\n", - "`CoordinodeClient`. The `_EmbeddedAdapter` below adds the extra methods that\n", - "`CoordinodePropertyGraphStore` expects when it receives a pre-built `client=` object." + "`LocalClient` (embedded engine) has the same `.cypher()` API as `CoordinodeClient`.\n", + "The `_EmbeddedAdapter` below adds the extra methods that the graph store expects\n", + "when it receives a pre-built `client=` object." ] }, { @@ -158,53 +146,39 @@ "metadata": {}, "outputs": [], "source": [ - "import os, socket, subprocess, time\n", + "import os, socket\n", "\n", - "if IN_COLAB:\n", - " from coordinode_embedded import LocalClient\n", "\n", - " _lc = LocalClient(\":memory:\")\n", - " client = _EmbeddedAdapter(_lc)\n", - " print(\"Using embedded LocalClient (in-process)\")\n", - "else:\n", - " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", - "\n", - " def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - " if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", - " elif _port_open(GRPC_PORT):\n", - " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " else:\n", - " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", - " proc = subprocess.run(\n", - " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", - " )\n", - " if proc.returncode != 0:\n", - " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", - " container_id = proc.stdout.strip()\n", - " for _ in range(30):\n", - " if _port_open(GRPC_PORT):\n", - " break\n", - " time.sleep(1)\n", - " else:\n", - " subprocess.run([\"docker\", \"stop\", container_id])\n", - " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")" + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " # Works without Docker or any external service; data is in-memory.\n", + " from coordinode_embedded import LocalClient\n", + "\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { @@ -333,7 +307,7 @@ " \"MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)\"\n", " \" WHERE p.name STARTS WITH $prefix\"\n", " \" RETURN p.name AS person, t.name AS topic, r.since AS since\",\n", - " param_map={\"prefix\": f\"Alice-{tag[:4]}\"},\n", + " param_map={\"prefix\": f\"Alice-{tag}\"},\n", ")\n", "print(\"Query result:\", rows)" ] diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 2a63b4d..86f6ae3 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -41,48 +41,26 @@ "\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain-coordinode\",\n", - " \"langchain\",\n", - " \"langchain-openai\",\n", - " \"langchain-community\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", - "else:\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"langchain-coordinode\",\n", - " \"langchain\",\n", - " \"langchain-openai\",\n", - " \"langchain-community\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " ],\n", + " check=True,\n", + ")\n", "\n", "print(\"SDK installed\")" ] @@ -94,9 +72,9 @@ "source": [ "## Adapter for embedded mode\n", "\n", - "In Colab we use `LocalClient` (embedded engine) which has the same `.cypher()` API as\n", - "`CoordinodeClient`. The `_EmbeddedAdapter` below adds the extra methods that\n", - "`CoordinodeGraph` expects when it receives a pre-built `client=` object." + "`LocalClient` (embedded engine) has the same `.cypher()` API as `CoordinodeClient`.\n", + "The `_EmbeddedAdapter` below adds the extra methods that `CoordinodeGraph` expects\n", + "when it receives a pre-built `client=` object." ] }, { @@ -159,53 +137,38 @@ "metadata": {}, "outputs": [], "source": [ - "import os, socket, subprocess, time\n", + "import os, socket\n", "\n", - "if IN_COLAB:\n", - " from coordinode_embedded import LocalClient\n", "\n", - " _lc = LocalClient(\":memory:\")\n", - " client = _EmbeddedAdapter(_lc)\n", - " print(\"Using embedded LocalClient (in-process)\")\n", - "else:\n", - " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", - "\n", - " def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - " if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", - " elif _port_open(GRPC_PORT):\n", - " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " else:\n", - " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", - " proc = subprocess.run(\n", - " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", - " )\n", - " if proc.returncode != 0:\n", - " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", - " container_id = proc.stdout.strip()\n", - " for _ in range(30):\n", - " if _port_open(GRPC_PORT):\n", - " break\n", - " time.sleep(1)\n", - " else:\n", - " subprocess.run([\"docker\", \"stop\", container_id])\n", - " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", + "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")" + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " from coordinode_embedded import LocalClient\n", + "\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { @@ -289,7 +252,7 @@ " \"MATCH (s:Scientist)-[r]->(f:Field)\"\n", " \" WHERE s.name STARTS WITH $prefix\"\n", " \" RETURN s.name AS scientist, type(r) AS relation, f.name AS field\",\n", - " params={\"prefix\": f\"Turing-{tag[:4]}\"},\n", + " params={\"prefix\": f\"Turing-{tag}\"},\n", ")\n", "print(\"Scientists → Fields:\")\n", "for r in rows:\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index a37c4f1..7d0d7a3 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -42,48 +42,27 @@ "\n", "IN_COLAB = \"google.colab\" in sys.modules\n", "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", "if IN_COLAB:\n", " # Install Rust toolchain — required to build coordinode-embedded from source\n", " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - " # Install coordinode-embedded from GitHub source (~5 min first run, cached after)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain\",\n", - " \"langchain-openai\",\n", - " \"langchain-community\",\n", - " \"langgraph\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", - "else:\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"langchain\",\n", - " \"langchain-openai\",\n", - " \"langchain-community\",\n", - " \"langgraph\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " )\n", - "\n", - "import nest_asyncio\n", "\n", - "nest_asyncio.apply()\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " \"langgraph\",\n", + " ],\n", + " check=True,\n", + ")\n", "\n", "print(\"SDK installed\")" ] @@ -106,56 +85,37 @@ "metadata": {}, "outputs": [], "source": [ - "import os, socket, subprocess, time\n", + "import os, socket\n", "\n", - "# CONTAINER_ID tracks a Docker container started by this notebook so the\n", - "# cleanup cell can stop it. None means no container was auto-started.\n", - "CONTAINER_ID = None\n", "\n", - "if IN_COLAB:\n", - " from coordinode_embedded import LocalClient\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")\n", - "else:\n", - " GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - " IMAGE = \"ghcr.io/structured-world/coordinode:latest\"\n", - "\n", - " def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - " if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " print(f\"Using COORDINODE_ADDR from environment: {COORDINODE_ADDR}\")\n", - " elif _port_open(GRPC_PORT):\n", - " print(f\"CoordiNode already reachable on :{GRPC_PORT}\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " else:\n", - " print(f\"Starting CoordiNode via Docker on :{GRPC_PORT} …\")\n", - " proc = subprocess.run(\n", - " [\"docker\", \"run\", \"-d\", \"--rm\", \"-p\", f\"{GRPC_PORT}:7080\", IMAGE], capture_output=True, text=True\n", - " )\n", - " if proc.returncode != 0:\n", - " raise RuntimeError(\"docker run failed: \" + proc.stderr)\n", - " CONTAINER_ID = proc.stdout.strip()\n", - " for _ in range(30):\n", - " if _port_open(GRPC_PORT):\n", - " break\n", - " time.sleep(1)\n", - " else:\n", - " subprocess.run([\"docker\", \"stop\", CONTAINER_ID])\n", - " raise RuntimeError(\"CoordiNode did not start in 30 s\")\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n", "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")" + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " from coordinode_embedded import LocalClient\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { @@ -211,8 +171,11 @@ " upper_q = q.upper()\n", " if any(kw in upper_q for kw in _WRITE_KEYWORDS):\n", " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " if \"$sess\" not in q:\n", - " return \"Query must include $sess to restrict results to the current session.\"\n", + " # Require $sess in a WHERE clause binding a node property, not just in any position.\n", + " # A query like 'RETURN $sess AS s' passes the token check but doesn't restrict data.\n", + " _SESS_WHERE_RE = re.compile(r\"WHERE\\b.*\\.session\\s*=\\s*\\$sess\", re.IGNORECASE | re.DOTALL)\n", + " if not _SESS_WHERE_RE.search(q):\n", + " return \"Query must restrict a node .session property in WHERE: WHERE .session = $sess\"\n", " rows = client.cypher(q, params={\"sess\": SESSION})\n", " return str(rows[:20]) if rows else \"No results\"\n", "\n", @@ -412,10 +375,7 @@ "source": [ "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", "print(\"Cleaned up session:\", SESSION)\n", - "client.close()\n", - "if CONTAINER_ID:\n", - " subprocess.run([\"docker\", \"stop\", CONTAINER_ID], check=False)\n", - " print(f\"Stopped container {CONTAINER_ID}\")" + "client.close()" ] } ], diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 3b63d04..196b530 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -79,8 +79,13 @@ def refresh_schema(self) -> None: (available since ``coordinode`` 0.6.0) over the legacy text-parsing path. Injected clients (e.g. ``_EmbeddedAdapter`` in Colab notebooks) that do not expose those methods fall back to ``_parse_schema()``. + Injected clients that only expose ``cypher()`` / ``close()`` (e.g. + a bare ``coordinode-embedded`` ``LocalClient``) return an empty schema. """ - self._schema = self._client.get_schema_text() + if hasattr(self._client, "get_schema_text"): + self._schema = self._client.get_schema_text() + else: + self._schema = "" if hasattr(self._client, "get_labels") and hasattr(self._client, "get_edge_types"): node_props: dict[str, list[dict[str, str]]] = {} @@ -242,6 +247,10 @@ def similarity_search( # used in a boolean context. len() == 0 works for all sequence types. if len(query_vector) == 0: return [] + if not hasattr(self._client, "vector_search"): + # Injected clients (e.g. bare coordinode-embedded LocalClient) may + # not implement vector_search — return empty rather than AttributeError. + return [] results = sorted( self._client.vector_search( label=label, @@ -256,11 +265,12 @@ def similarity_search( # ── Lifecycle ───────────────────────────────────────────────────────── def close(self) -> None: - """Close the underlying gRPC connection. + """Close the underlying client connection. Only closes the client if it was created internally (i.e. ``client`` was not passed to ``__init__``). Externally-injected clients are owned by - the caller and must be closed by them. + the caller and must be closed by them. The underlying transport may be + gRPC or an embedded in-process client. """ if self._owns_client: self._client.close() diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index cd9820b..39285ab 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -273,6 +273,11 @@ def vector_query( if query.query_embedding is None: return [], [] + if not hasattr(self._client, "vector_search"): + # Injected clients (e.g. bare coordinode-embedded LocalClient) may + # not implement vector_search — return empty rather than AttributeError. + return [], [] + results = self._client.vector_search( label=query.filters.filters[0].value if query.filters else "Chunk", property="embedding", @@ -297,18 +302,26 @@ def vector_query( # ── Lifecycle ───────────────────────────────────────────────────────── def get_schema(self, refresh: bool = False) -> str: - """Return schema as text.""" - return self._client.get_schema_text() + """Return schema as text. + + Returns an empty string if the injected client does not expose + ``get_schema_text()`` (e.g. a bare ``coordinode-embedded`` + ``LocalClient`` that only implements ``cypher()`` / ``close()``). + """ + if hasattr(self._client, "get_schema_text"): + return self._client.get_schema_text() + return "" def get_schema_str(self, refresh: bool = False) -> str: return self.get_schema(refresh=refresh) def close(self) -> None: - """Close the underlying gRPC connection. + """Close the underlying client connection. Only closes the client if it was created internally (i.e. ``client`` was not passed to ``__init__``). Externally-injected clients are owned by - the caller and must be closed by them. + the caller and must be closed by them. The underlying transport may be + gRPC or an embedded in-process client. """ if self._owns_client: self._client.close() diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index d5043ef..47323ea 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -461,8 +461,9 @@ def test_create_label_appears_in_get_labels(client): """ name = f"CreateLabelVisible{uid()}" tag = uid() - # Declare both properties used in the workaround node so the strict label - # does not reject the CREATE (server now enforces strict schema). + # Declare both properties used in the workaround node. Note: schema_mode + # is accepted by the server but not yet enforced in the current image; + # the properties are declared here for forward compatibility. client.create_label( name, properties=[ diff --git a/tests/unit/test_schema_crud.py b/tests/unit/test_schema_crud.py index 398a7e7..57880e1 100644 --- a/tests/unit/test_schema_crud.py +++ b/tests/unit/test_schema_crud.py @@ -248,6 +248,103 @@ def test_repr_shows_counts(self): # ── traverse() input validation ────────────────────────────────────────────── +class _FakePropertyTypeAll: + """Complete fake proto PropertyType with all enum values.""" + + PROPERTY_TYPE_INT64 = 1 + PROPERTY_TYPE_FLOAT64 = 2 + PROPERTY_TYPE_STRING = 3 + PROPERTY_TYPE_BOOL = 4 + PROPERTY_TYPE_BYTES = 5 + PROPERTY_TYPE_TIMESTAMP = 6 + PROPERTY_TYPE_VECTOR = 7 + PROPERTY_TYPE_LIST = 8 + PROPERTY_TYPE_MAP = 9 + + +class _FakePropDefCls: + """Minimal PropertyDefinition constructor.""" + + def __init__(self, **kwargs): + pass + + +class TestBuildPropertyDefinitions: + """Unit tests for AsyncCoordinodeClient._build_property_definitions() validation. + + Validation runs before any RPC call, so no running server is required. + """ + + def test_non_dict_property_raises(self): + """_build_property_definitions() raises ValueError for non-dict entries.""" + client = AsyncCoordinodeClient("localhost:0") + with pytest.raises(ValueError, match="must be a dict"): + client._build_property_definitions(["not-a-dict"], _FakePropertyTypeAll, _FakePropDefCls) + + def test_missing_name_raises(self): + """_build_property_definitions() raises ValueError when 'name' key is absent.""" + client = AsyncCoordinodeClient("localhost:0") + with pytest.raises(ValueError, match="non-empty 'name' key"): + client._build_property_definitions([{"type": "string"}], _FakePropertyTypeAll, _FakePropDefCls) + + def test_non_bool_required_raises(self): + """_build_property_definitions() raises ValueError when required is not a bool.""" + client = AsyncCoordinodeClient("localhost:0") + with pytest.raises(ValueError, match="boolean values for 'required' and 'unique'"): + client._build_property_definitions( + [{"name": "x", "type": "string", "required": "true"}], + _FakePropertyTypeAll, + _FakePropDefCls, + ) + + def test_non_bool_unique_raises(self): + """_build_property_definitions() raises ValueError when unique is not a bool.""" + client = AsyncCoordinodeClient("localhost:0") + with pytest.raises(ValueError, match="boolean values for 'required' and 'unique'"): + client._build_property_definitions( + [{"name": "x", "type": "string", "unique": 1}], + _FakePropertyTypeAll, + _FakePropDefCls, + ) + + def test_valid_bool_properties_accepted(self): + """_build_property_definitions() accepts proper bool required/unique values.""" + client = AsyncCoordinodeClient("localhost:0") + result = client._build_property_definitions( + [{"name": "x", "type": "string", "required": True, "unique": False}], + _FakePropertyTypeAll, + _FakePropDefCls, + ) + assert len(result) == 1 + + +class TestCreateLabelSchemaMode: + """Unit tests for schema_mode normalization in create_label().""" + + def test_invalid_schema_mode_raises(self): + """create_label() raises ValueError for unknown schema_mode string.""" + + async def _inner() -> None: + client = AsyncCoordinodeClient("localhost:0") + with pytest.raises(ValueError, match="schema_mode must be one of"): + await client.create_label("Foo", schema_mode="unknown") + + asyncio.run(_inner()) + + def test_uppercase_schema_mode_accepted(self): + """create_label() normalizes 'STRICT' to 'strict' before lookup.""" + + async def _inner() -> None: + client = AsyncCoordinodeClient("localhost:0") + # Should raise ValueError (unknown mode), not KeyError (normalization works) + with pytest.raises(ValueError, match="schema_mode must be one of"): + await client.create_label("Foo", schema_mode="totally_wrong") + # 'STRICT' is a valid mode and should NOT raise + # (will fail at RPC level, not at validation — we can't test past validation here) + + asyncio.run(_inner()) + + class TestTraverseValidation: """Unit tests for AsyncCoordinodeClient.traverse() input validation. From bb1d440233a9d563928db8daa97ad1dfac0bfd12 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 13 Apr 2026 23:11:59 +0300 Subject: [PATCH 15/86] fix(client,graph,tests,notebooks): schema_mode guard, docstring fixes, regex safety - client.py: add isinstance(str) guard before schema_mode.strip() - client.py: remove false "capped at 1000" claim from text_search/hybrid docstrings - graph.py: fall back to _parse_schema when get_labels/get_edge_types return empty - test_sdk.py: narrow _fts xfail to raises=(AssertionError, grpc.RpcError) - test_schema_crud.py: exercise STRICT normalisation via AsyncMock stub - notebook 00: add coordinode to pip install for non-Colab path - notebook 03: replace _WRITE_KEYWORDS tuple with word-boundary regex; extend _SESSION_SCOPE_RE to accept {session: $sess} node patterns --- coordinode/coordinode/client.py | 8 ++++-- demo/notebooks/00_seed_data.ipynb | 1 + demo/notebooks/03_langgraph_agent.ipynb | 28 ++++++++++++------- .../langchain_coordinode/graph.py | 7 ++++- tests/integration/test_sdk.py | 2 ++ tests/unit/test_schema_crud.py | 17 +++++++---- 6 files changed, 44 insertions(+), 19 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 6a984d6..1c9e28b 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -477,6 +477,8 @@ async def create_label( "validated": SchemaMode.SCHEMA_MODE_VALIDATED, "flexible": SchemaMode.SCHEMA_MODE_FLEXIBLE, } + if not isinstance(schema_mode, str): + raise ValueError(f"schema_mode must be a str, got {type(schema_mode).__name__!r}") schema_mode_normalized = schema_mode.strip().lower() if schema_mode_normalized not in _mode_map: raise ValueError(f"schema_mode must be one of {list(_mode_map)}, got {schema_mode!r}") @@ -583,7 +585,8 @@ async def text_search( query: Full-text query string. Supports boolean operators (``AND``, ``OR``, ``NOT``), phrase search (``"exact phrase"``), prefix wildcards (``term*``), and per-term boosting (``term^N``). - limit: Maximum results to return (default 10, capped at 1000). + limit: Maximum results to return (default 10). The server may apply + its own upper bound; pass a reasonable value (e.g. ≤ 1000). fuzzy: If ``True``, apply Levenshtein-1 fuzzy matching to individual terms. Increases recall at the cost of precision. language: Tokenization/stemming language (e.g. ``"english"``, @@ -622,7 +625,8 @@ async def hybrid_text_vector_search( text_query: Full-text query string (same syntax as :meth:`text_search`). vector: Query embedding vector. Must match the dimensionality stored in *vector_property*. - limit: Maximum fused results to return (default 10, capped at 1000). + limit: Maximum fused results to return (default 10). The server may + apply its own upper bound; pass a reasonable value (e.g. ≤ 1000). text_weight: Weight for the BM25 component (default 0.5). vector_weight: Weight for the cosine component (default 0.5). vector_property: Node property containing the embedding (default diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index aa9877e..6b23de3 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -58,6 +58,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", + " \"coordinode\",\n", " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", " \"nest_asyncio\",\n", " ],\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 7d0d7a3..0fc7524 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -142,8 +142,15 @@ "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", "\n", "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", - "# Write keywords that query_facts must not execute (demo safety guard).\n", - "_WRITE_KEYWORDS = (\"CREATE \", \"MERGE \", \"DELETE \", \"DETACH \", \"SET \", \"REMOVE \", \"DROP \")\n", + "# Regex guards for query_facts (demo safety guard).\n", + "_WRITE_CLAUSE_RE = re.compile(\n", + " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", + " re.IGNORECASE,\n", + ")\n", + "_SESSION_SCOPE_RE = re.compile(\n", + " r\"WHERE\\b.*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", "\n", "\n", "@tool\n", @@ -168,14 +175,15 @@ " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", " Must include $sess filtering: WHERE n.session = $sess\"\"\"\n", " q = cypher.strip()\n", - " upper_q = q.upper()\n", - " if any(kw in upper_q for kw in _WRITE_KEYWORDS):\n", + " if _WRITE_CLAUSE_RE.search(q):\n", " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " # Require $sess in a WHERE clause binding a node property, not just in any position.\n", - " # A query like 'RETURN $sess AS s' passes the token check but doesn't restrict data.\n", - " _SESS_WHERE_RE = re.compile(r\"WHERE\\b.*\\.session\\s*=\\s*\\$sess\", re.IGNORECASE | re.DOTALL)\n", - " if not _SESS_WHERE_RE.search(q):\n", - " return \"Query must restrict a node .session property in WHERE: WHERE .session = $sess\"\n", + " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", + " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", + " if not _SESSION_SCOPE_RE.search(q):\n", + " return (\n", + " \"Query must scope reads to the current session with either \"\n", + " \"WHERE .session = $sess or {session: $sess}\"\n", + " )\n", " rows = client.cypher(q, params={\"sess\": SESSION})\n", " return str(rows[:20]) if rows else \"No results\"\n", "\n", @@ -391,4 +399,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 196b530..da68824 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -99,7 +99,12 @@ def refresh_schema(self) -> None: rel_props[et.name] = [ {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} for p in et.properties ] - structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} + if node_props or rel_props: + structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} + else: + # Both APIs returned empty (e.g. schema-free graph or stub adapter) — + # fall back to text parsing so we don't lose what get_schema_text() returned. + structured = _parse_schema(self._schema) else: structured = _parse_schema(self._schema) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 47323ea..5214818 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -10,6 +10,7 @@ import os import uuid +import grpc import pytest from coordinode import ( @@ -555,6 +556,7 @@ def test_vector_search_returns_results(client): _fts = pytest.mark.xfail( reason="TextService requires CoordiNode >=0.3.8 with FTS support", strict=False, + raises=(AssertionError, grpc.RpcError), ) diff --git a/tests/unit/test_schema_crud.py b/tests/unit/test_schema_crud.py index 57880e1..6109a3b 100644 --- a/tests/unit/test_schema_crud.py +++ b/tests/unit/test_schema_crud.py @@ -332,15 +332,20 @@ async def _inner() -> None: asyncio.run(_inner()) def test_uppercase_schema_mode_accepted(self): - """create_label() normalizes 'STRICT' to 'strict' before lookup.""" + """create_label() normalizes ' STRICT ' (with spaces and uppercase) to 'strict' before RPC.""" + from unittest.mock import AsyncMock async def _inner() -> None: client = AsyncCoordinodeClient("localhost:0") - # Should raise ValueError (unknown mode), not KeyError (normalization works) - with pytest.raises(ValueError, match="schema_mode must be one of"): - await client.create_label("Foo", schema_mode="totally_wrong") - # 'STRICT' is a valid mode and should NOT raise - # (will fail at RPC level, not at validation — we can't test past validation here) + # Patch the schema stub so the RPC call doesn't reach a real server. + client._schema_stub = type( + "FakeStub", + (), + {"CreateLabel": AsyncMock(return_value=_FakeLabel("Foo"))}, + )() + # ' STRICT ' must normalise cleanly (strip + lower) and NOT raise ValueError. + info = await client.create_label("Foo", schema_mode=" STRICT ") + assert info.name == "Foo" asyncio.run(_inner()) From 58f43581018758925f0888a12397b6fe09b6f19e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 00:07:17 +0300 Subject: [PATCH 16/86] fix(notebooks,graph,tests): add coordinode install, client guard, code clarity - demo/notebooks/03: add "coordinode" to pip install list so CoordinodeClient import succeeds when COORDINODE_ADDR is set or gRPC port is detected - langchain_coordinode/graph.py: raise TypeError early when injected client lacks callable cypher(); add code comment explaining why _PROPERTY_TYPE_NAME is a static dict (avoid proto import dependency inside langchain package) - llama_index/graph_stores/coordinode/base.py: same TypeError guard for cypher() - tests/integration/test_sdk.py: add code comment explaining why AssertionError is included in xfail raises= (observed failure mode when FTS absent) - tests/unit/test_schema_crud.py: add comment to _FakePropDefCls.__init__ pass (SonarCloud empty-body warning) --- demo/notebooks/03_langgraph_agent.ipynb | 3 ++- langchain-coordinode/langchain_coordinode/graph.py | 6 ++++++ .../llama_index/graph_stores/coordinode/base.py | 2 ++ tests/integration/test_sdk.py | 5 +++++ tests/unit/test_schema_crud.py | 2 +- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 0fc7524..f30adae 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -55,6 +55,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", + " \"coordinode\",\n", " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", " \"langchain-coordinode\",\n", " \"langchain-community\",\n", @@ -399,4 +400,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index da68824..bcd3173 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -51,6 +51,8 @@ def __init__( # coordinode-embedded) instead of creating a gRPC connection. The object # must expose a ``.cypher(query, params)`` method and, optionally, # ``.get_schema_text()`` and ``.vector_search()``. + if client is not None and not callable(getattr(client, "cypher", None)): + raise TypeError("client must provide a callable cypher(query, params) method") self._owns_client = client is None self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) self._schema: str | None = None @@ -291,6 +293,10 @@ def __exit__(self, *args: Any) -> None: # Maps PropertyType protobuf enum integers to LangChain-compatible type strings. # Values mirror coordinode.v1.graph.PropertyType (schema.proto). +# A static dict is intentional: importing generated proto modules here would create +# a hard dependency on coordinode's internal proto layout inside langchain-coordinode. +# This package already receives integer enum values via LabelInfo/EdgeTypeInfo from +# the coordinode SDK, so a local lookup table is the correct decoupling boundary. _PROPERTY_TYPE_NAME: dict[int, str] = { 0: "UNSPECIFIED", 1: "INT64", diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 39285ab..ac6dbe8 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -72,6 +72,8 @@ def __init__( ) -> None: # ``client`` allows passing a pre-built client (e.g. LocalClient from # coordinode-embedded) instead of creating a gRPC connection. + if client is not None and not callable(getattr(client, "cypher", None)): + raise TypeError("client must provide a callable cypher() method") self._owns_client = client is None self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 5214818..7750851 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -556,6 +556,11 @@ def test_vector_search_returns_results(client): _fts = pytest.mark.xfail( reason="TextService requires CoordiNode >=0.3.8 with FTS support", strict=False, + # AssertionError is the actual observed failure mode on servers without FTS: + # text_search() returns [] (empty result set), triggering `assert len(...) >= 1`. + # grpc.RpcError covers servers that raise UNIMPLEMENTED. Both are expected until + # the server is upgraded; removing AssertionError would cause those tests to + # error-out (unexpected failure) rather than xfail. raises=(AssertionError, grpc.RpcError), ) diff --git a/tests/unit/test_schema_crud.py b/tests/unit/test_schema_crud.py index 6109a3b..67db15c 100644 --- a/tests/unit/test_schema_crud.py +++ b/tests/unit/test_schema_crud.py @@ -266,7 +266,7 @@ class _FakePropDefCls: """Minimal PropertyDefinition constructor.""" def __init__(self, **kwargs): - pass + pass # Stub: kwargs intentionally ignored — only used to verify call succeeds class TestBuildPropertyDefinitions: From 81163763cd7c6db0775f46895d65ae09a677e021 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 00:15:09 +0300 Subject: [PATCH 17/86] fix(notebooks): replace .* with [^;{}]* in SESSION_SCOPE_RE to prevent ReDoS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unbounded .* with re.DOTALL causes polynomial backtracking on long queries that contain WHERE but no matching .session token. [^;{}]* is bounded by natural Cypher delimiters (semicolons, braces) — O(n) worst case. Also drops re.DOTALL since WHERE clauses are single-line in practice. --- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index f30adae..635f9c7 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -149,8 +149,8 @@ " re.IGNORECASE,\n", ")\n", "_SESSION_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b.*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", - " re.IGNORECASE | re.DOTALL,\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", + " re.IGNORECASE,\n", ")\n", "\n", "\n", From 64e45dd9e4d8f6752297ac750a55d039ea29a797 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 01:24:03 +0300 Subject: [PATCH 18/86] docs: document client param in CoordinodeGraph and CoordinodePropertyGraphStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add client kwarg to Args section of both class docstrings - Wrap get_labels()/get_edge_types() in try/except in refresh_schema() so gRPC UNIMPLEMENTED from older servers falls back to _parse_schema() instead of surfacing an unhandled exception - Add code comment on _SESSION_SCOPE_RE explaining the known Cartesian-product bypass (full per-alias check requires Cypher AST parser — out of scope for a demo safety guard) - Add code comment on FTS integration tests explaining why create_label() is not called before text_search() (schema-free path is intentional) --- demo/notebooks/03_langgraph_agent.ipynb | 87 +------------------ .../langchain_coordinode/graph.py | 45 ++++++---- .../graph_stores/coordinode/base.py | 5 ++ tests/integration/test_sdk.py | 8 ++ 4 files changed, 44 insertions(+), 101 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 635f9c7..cff0ef6 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -136,90 +136,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": [ - "import os, re, uuid\n", - "from langchain_core.tools import tool\n", - "\n", - "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", - "\n", - "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", - "# Regex guards for query_facts (demo safety guard).\n", - "_WRITE_CLAUSE_RE = re.compile(\n", - " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", - " re.IGNORECASE,\n", - ")\n", - "_SESSION_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", - " re.IGNORECASE,\n", - ")\n", - "\n", - "\n", - "@tool\n", - "def save_fact(subject: str, relation: str, obj: str) -> str:\n", - " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", - " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", - " rel_type = relation.upper().replace(\" \", \"_\")\n", - " # Validate rel_type before interpolating into Cypher to prevent injection.\n", - " if not _REL_TYPE_RE.fullmatch(rel_type):\n", - " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", - " client.cypher(\n", - " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", - " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", - " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", - " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", - " )\n", - " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", - "\n", - "\n", - "@tool\n", - "def query_facts(cypher: str) -> str:\n", - " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", - " Must include $sess filtering: WHERE n.session = $sess\"\"\"\n", - " q = cypher.strip()\n", - " if _WRITE_CLAUSE_RE.search(q):\n", - " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", - " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", - " if not _SESSION_SCOPE_RE.search(q):\n", - " return (\n", - " \"Query must scope reads to the current session with either \"\n", - " \"WHERE .session = $sess or {session: $sess}\"\n", - " )\n", - " rows = client.cypher(q, params={\"sess\": SESSION})\n", - " return str(rows[:20]) if rows else \"No results\"\n", - "\n", - "\n", - "@tool\n", - "def find_related(entity_name: str, depth: int = 1) -> str:\n", - " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", - " safe_depth = max(1, min(int(depth), 3))\n", - " rows = client.cypher(\n", - " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n", - " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", - " params={\"name\": entity_name, \"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return f\"No related entities found for {entity_name}\"\n", - " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", - "\n", - "\n", - "@tool\n", - "def list_all_facts() -> str:\n", - " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", - " rows = client.cypher(\n", - " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", - " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", - " params={\"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return \"No facts stored yet\"\n", - " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", - "\n", - "\n", - "tools = [save_fact, query_facts, find_related, list_all_facts]\n", - "print(f\"Session: {SESSION}\")\n", - "print(\"Tools:\", [t.name for t in tools])" - ] + "source": "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n re.IGNORECASE,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must include $sess filtering: WHERE n.session = $sess\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not _SESSION_SCOPE_RE.search(q):\n return (\n \"Query must scope reads to the current session with either \"\n \"WHERE .session = $sess or {session: $sess}\"\n )\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" }, { "cell_type": "markdown", @@ -400,4 +317,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index bcd3173..3aee067 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -37,6 +37,11 @@ class CoordinodeGraph(GraphStore): addr: CoordiNode gRPC address, e.g. ``"localhost:7080"``. database: Database name (reserved for future multi-db support). timeout: Per-request gRPC deadline in seconds. + client: Optional pre-built client object (e.g. ``LocalClient`` from + ``coordinode-embedded``) to use instead of creating a gRPC connection. + Must expose a callable ``cypher(query, params)`` method. When + provided, ``addr`` and ``timeout`` are ignored. The caller is + responsible for closing the client. """ def __init__( @@ -90,22 +95,30 @@ def refresh_schema(self) -> None: self._schema = "" if hasattr(self._client, "get_labels") and hasattr(self._client, "get_edge_types"): - node_props: dict[str, list[dict[str, str]]] = {} - for label in self._client.get_labels(): - node_props[label.name] = [ - {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} - for p in label.properties - ] - rel_props: dict[str, list[dict[str, str]]] = {} - for et in self._client.get_edge_types(): - rel_props[et.name] = [ - {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} for p in et.properties - ] - if node_props or rel_props: - structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} - else: - # Both APIs returned empty (e.g. schema-free graph or stub adapter) — - # fall back to text parsing so we don't lose what get_schema_text() returned. + try: + node_props: dict[str, list[dict[str, str]]] = {} + for label in self._client.get_labels(): + node_props[label.name] = [ + {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} + for p in label.properties + ] + rel_props: dict[str, list[dict[str, str]]] = {} + for et in self._client.get_edge_types(): + rel_props[et.name] = [ + {"property": p.name, "type": _PROPERTY_TYPE_NAME.get(p.type, "UNSPECIFIED")} + for p in et.properties + ] + if node_props or rel_props: + structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} + else: + # Both APIs returned empty (e.g. schema-free graph or stub adapter) — + # fall back to text parsing so we don't lose what get_schema_text() returned. + structured = _parse_schema(self._schema) + except Exception: + # Server may expose get_labels/get_edge_types but raise UNIMPLEMENTED + # or another gRPC error if the schema service is not available in the + # deployed version. Fall back to text-based schema parsing to avoid + # surfacing an unhandled exception to the caller. structured = _parse_schema(self._schema) else: structured = _parse_schema(self._schema) diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index ac6dbe8..33257a1 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -58,6 +58,11 @@ class CoordinodePropertyGraphStore(PropertyGraphStore): Args: addr: CoordiNode gRPC address, e.g. ``"localhost:7080"``. timeout: Per-request gRPC deadline in seconds. + client: Optional pre-built client object (e.g. ``LocalClient`` from + ``coordinode-embedded``) to use instead of creating a gRPC connection. + Must expose a callable ``cypher(query, params)`` method. When + provided, ``addr`` and ``timeout`` are ignored. The caller is + responsible for closing the client. """ supports_structured_queries: bool = True diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 7750851..ad5700b 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -553,6 +553,14 @@ def test_vector_search_returns_results(client): # FTS tests require a CoordiNode server with TextService implemented (>=0.3.8). # They are marked xfail so the suite stays green against older servers; once # upgraded, the tests turn into expected passes automatically. +# +# Note: create_label() is intentionally NOT called before text_search(). +# FTS indexing in CoordiNode is automatic for all nodes whose label was written +# via CREATE/MERGE — no explicit label registration is required. On schema-strict +# servers a caller may choose to pre-register a label, but the SDK's text_search() +# and hybrid_text_vector_search() work on schema-free graphs too. These tests +# exercise the common schema-free path; calling create_label() here would test a +# different (schema-strict) code path and is covered by test_create_label_*. _fts = pytest.mark.xfail( reason="TextService requires CoordiNode >=0.3.8 with FTS support", strict=False, From f70bb7350f74b939579405af07e16861fcadae7d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 01:53:04 +0300 Subject: [PATCH 19/86] fix: narrow exception handling and fix float cast in hybrid search - CoordinodePropertyGraphStore.__init__: set supports_vector_queries per instance based on callable(getattr(client, "vector_search", None)) so injected clients without vector_search() correctly advertise False instead of the unconditional class-level True - refresh_schema(): add debug logging before _parse_schema() fallback so the swallowed exception remains observable; expand comment explaining why broad Exception is intentional (no hard grpc dependency in langchain package) - hybrid_text_vector_search(): cast vector elements via [float(v) for v in vector] to match vector_search() and text_vector_search() convention; prevents breakage with numpy/decimal callers --- coordinode/coordinode/client.py | 2 +- langchain-coordinode/langchain_coordinode/graph.py | 12 ++++++++++++ .../llama_index/graph_stores/coordinode/base.py | 5 +++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 1c9e28b..7684958 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -642,7 +642,7 @@ async def hybrid_text_vector_search( req = HybridTextVectorSearchRequest( label=label, text_query=text_query, - vector=list(vector), + vector=[float(v) for v in vector], limit=limit, text_weight=text_weight, vector_weight=vector_weight, diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 3aee067..7f387e0 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -4,10 +4,13 @@ import hashlib import json +import logging import re from collections.abc import Sequence from typing import Any +logger = logging.getLogger(__name__) + from langchain_community.graphs.graph_store import GraphStore from coordinode import CoordinodeClient @@ -119,6 +122,15 @@ def refresh_schema(self) -> None: # or another gRPC error if the schema service is not available in the # deployed version. Fall back to text-based schema parsing to avoid # surfacing an unhandled exception to the caller. + # We catch broad Exception (rather than grpc.RpcError specifically) + # because langchain-coordinode does not take a hard grpc dependency — + # clients may be non-gRPC (e.g. coordinode-embedded LocalClient). + # The exception is logged at DEBUG so it remains observable without + # cluttering production output. + logger.debug( + "get_labels()/get_edge_types() failed — falling back to _parse_schema()", + exc_info=True, + ) structured = _parse_schema(self._schema) else: structured = _parse_schema(self._schema) diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 33257a1..53168c1 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -81,6 +81,11 @@ def __init__( raise TypeError("client must provide a callable cypher() method") self._owns_client = client is None self._client = client if client is not None else CoordinodeClient(addr, timeout=timeout) + # Advertise vector capability based on what the actual client exposes. + # Injected clients (e.g. bare coordinode-embedded LocalClient) may not + # implement vector_search(); claiming True unconditionally would mislead + # LlamaIndex into passing vector queries that silently return no results. + self.supports_vector_queries = callable(getattr(self._client, "vector_search", None)) # ── Node operations ─────────────────────────────────────────────────── From 60d77cc97261d015affff85dff200e295e8850b0 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 02:01:05 +0300 Subject: [PATCH 20/86] style: fix ruff lint errors (E402, UP038) and tighten ReDoS-safe regex - Move logger instantiation after all imports to fix E402 in graph.py - Use `X | Y` union syntax in isinstance() calls in _types.py (UP038) - Add `\n` to regex char class `[^)\n]+` in _parse_schema() to make line-boundedness explicit and address Copilot ReDoS concern --- coordinode/coordinode/_types.py | 4 ++-- langchain-coordinode/langchain_coordinode/graph.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/coordinode/coordinode/_types.py b/coordinode/coordinode/_types.py index ef45edd..dbfa035 100644 --- a/coordinode/coordinode/_types.py +++ b/coordinode/coordinode/_types.py @@ -33,11 +33,11 @@ def to_property_value(py_val: PyValue) -> Any: pv.string_value = py_val elif isinstance(py_val, bytes): pv.bytes_value = py_val - elif isinstance(py_val, (list, tuple)): + elif isinstance(py_val, list | tuple): # Homogeneous float list → Vector; mixed/str list → PropertyList. # bool is a subclass of int, so exclude it explicitly — [True, False] must # not be serialised as a Vector of 1.0/0.0 but as a PropertyList. - if py_val and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in py_val): + if py_val and all(isinstance(v, int | float) and not isinstance(v, bool) for v in py_val): vec = Vector(values=[float(v) for v in py_val]) pv.vector_value.CopyFrom(vec) else: diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 7f387e0..8bec498 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -9,12 +9,12 @@ from collections.abc import Sequence from typing import Any -logger = logging.getLogger(__name__) - from langchain_community.graphs.graph_store import GraphStore from coordinode import CoordinodeClient +logger = logging.getLogger(__name__) + class CoordinodeGraph(GraphStore): """LangChain `GraphStore` backed by CoordiNode. @@ -438,7 +438,9 @@ def _parse_schema(schema_text: str) -> dict[str, Any]: continue # Parse inline properties: "- Label (properties: prop1: TYPE, prop2: TYPE)" props: list[dict[str, str]] = [] - m = re.search(r"\(properties:\s*([^)]+)\)", stripped) + # [^)\n]+ matches property list characters without crossing line or ')'. + # Each character in the class is a simple literal — no backtracking risk. + m = re.search(r"\(properties:\s*([^)\n]+)\)", stripped) if m: for prop_str in m.group(1).split(","): kv = prop_str.strip().split(":", 1) From 92dda39ab19c71e79b8fb9df4f1f4230962385ab Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 02:16:18 +0300 Subject: [PATCH 21/86] fix: use callable(getattr()) instead of hasattr() for optional client methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hasattr() checks with callable(getattr(client, method, None)) for get_labels/get_edge_types/get_schema_text in langchain graph.py and vector_search/get_schema_text in llama-index base.py — hasattr() returns True for non-callable attributes, callable() guarantees the attribute can be invoked before use - Use self.supports_vector_queries (already computed via callable() in __init__) in vector_query() instead of a redundant hasattr() check - Add explanatory comment in Colab install cells: apt-get rustc ≤1.75 cannot build coordinode-embedded (requires Rust ≥1.80); rustup is mandatory with --proto/--tlsv1.2 TLS enforcement --- demo/notebooks/00_seed_data.ipynb | 34 +---------------- .../01_llama_index_property_graph.ipynb | 37 +------------------ demo/notebooks/02_langchain_graph_chain.ipynb | 31 +--------------- demo/notebooks/03_langgraph_agent.ipynb | 31 +--------------- .../langchain_coordinode/graph.py | 12 +++--- .../graph_stores/coordinode/base.py | 8 ++-- 6 files changed, 18 insertions(+), 135 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 6b23de3..f20fb64 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -40,37 +40,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain — required to build coordinode-embedded from source\n", - " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"Ready\")" - ] + "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"nest_asyncio\",\n ],\n check=True,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -377,4 +347,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 2c38f46..2189648 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,40 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "# In Colab, build from source (~5 min first run, cached after).\n", - "# Locally, it installs in seconds if a pre-built wheel is cached.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain — required to build coordinode-embedded from source\n", - " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"llama-index-graph-stores-coordinode\",\n", - " \"llama-index-core\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\n# In Colab, build from source (~5 min first run, cached after).\n# Locally, it installs in seconds if a pre-built wheel is cached.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -388,4 +355,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 86f6ae3..599dbb0 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,34 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain — required to build coordinode-embedded from source\n", - " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -375,4 +348,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index cff0ef6..7ff0cdc 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,36 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain — required to build coordinode-embedded from source\n", - " subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " \"langgraph\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 8bec498..d0ac3dc 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -92,12 +92,12 @@ def refresh_schema(self) -> None: Injected clients that only expose ``cypher()`` / ``close()`` (e.g. a bare ``coordinode-embedded`` ``LocalClient``) return an empty schema. """ - if hasattr(self._client, "get_schema_text"): - self._schema = self._client.get_schema_text() - else: - self._schema = "" + get_schema_text = getattr(self._client, "get_schema_text", None) + self._schema = get_schema_text() if callable(get_schema_text) else "" - if hasattr(self._client, "get_labels") and hasattr(self._client, "get_edge_types"): + if callable(getattr(self._client, "get_labels", None)) and callable( + getattr(self._client, "get_edge_types", None) + ): try: node_props: dict[str, list[dict[str, str]]] = {} for label in self._client.get_labels(): @@ -279,7 +279,7 @@ def similarity_search( # used in a boolean context. len() == 0 works for all sequence types. if len(query_vector) == 0: return [] - if not hasattr(self._client, "vector_search"): + if not callable(getattr(self._client, "vector_search", None)): # Injected clients (e.g. bare coordinode-embedded LocalClient) may # not implement vector_search — return empty rather than AttributeError. return [] diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 53168c1..3cc65fd 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -285,9 +285,10 @@ def vector_query( if query.query_embedding is None: return [], [] - if not hasattr(self._client, "vector_search"): + if not self.supports_vector_queries: # Injected clients (e.g. bare coordinode-embedded LocalClient) may # not implement vector_search — return empty rather than AttributeError. + # supports_vector_queries is set in __init__ via callable(getattr(...)). return [], [] results = self._client.vector_search( @@ -320,8 +321,9 @@ def get_schema(self, refresh: bool = False) -> str: ``get_schema_text()`` (e.g. a bare ``coordinode-embedded`` ``LocalClient`` that only implements ``cypher()`` / ``close()``). """ - if hasattr(self._client, "get_schema_text"): - return self._client.get_schema_text() + get_schema_text = getattr(self._client, "get_schema_text", None) + if callable(get_schema_text): + return get_schema_text() return "" def get_schema_str(self, refresh: bool = False) -> str: From 83bf591dfabf0642656bb48160133e3a9cca2651 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 03:43:51 +0300 Subject: [PATCH 22/86] style: reflow xfail comment in _fts marker (no logic change) --- tests/integration/test_sdk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index ad5700b..6d4e505 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -566,9 +566,9 @@ def test_vector_search_returns_results(client): strict=False, # AssertionError is the actual observed failure mode on servers without FTS: # text_search() returns [] (empty result set), triggering `assert len(...) >= 1`. - # grpc.RpcError covers servers that raise UNIMPLEMENTED. Both are expected until - # the server is upgraded; removing AssertionError would cause those tests to - # error-out (unexpected failure) rather than xfail. + # grpc.RpcError covers servers that raise UNIMPLEMENTED. Both are expected + # until the server is upgraded; removing AssertionError would cause those tests + # to error-out (unexpected failure) rather than xfail. raises=(AssertionError, grpc.RpcError), ) From 04009121127993e9dfa05338d51633860a98bce5 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 04:03:22 +0300 Subject: [PATCH 23/86] fix(client,graph,notebooks): property validation, schema fallback, rustup via urllib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _build_property_definitions(): validate properties is list/tuple before iterating; passes None→[] early, raises clear TypeError on dict input instead of confusing 'Property at index 0 must be a dict; got str' error - CoordinodeGraph.refresh_schema(): wrap get_schema_text() in try/except so a failing custom client method degrades to empty schema text instead of aborting before the structured-API / _parse_schema() fallback can run; clarify docstring - CoordinodeGraph.refresh_schema() docstring: note that relationships may still be inferred via Cypher even when property metadata is unavailable - demo notebooks (all 4): replace curl|sh pipeline (shell=True) with urllib.request + NamedTemporaryFile + explicit /bin/sh invocation; removes shell=True with untrusted content; uses Python default ssl context (TLS 1.2+, cert-verified) — equivalent security to the former --tlsv1.2 curl flags - notebooks 01 (LlamaIndex) and 02 (LangChain): add explicit 'coordinode' to pip install list (was only a transitive dep via framework packages; now explicit) - notebook 00_seed_data: _label() now validates against all three known sets (src_names, tech_names, company_names) and raises ValueError for unknown names instead of silently defaulting to 'Company' - notebook 03_langgraph_agent: update query_facts docstring to document both accepted session-scope forms; merge implicitly concatenated error strings --- coordinode/coordinode/client.py | 6 +- demo/notebooks/00_seed_data.ipynb | 50 ++++++- .../01_llama_index_property_graph.ipynb | 50 ++++++- demo/notebooks/02_langchain_graph_chain.ipynb | 44 +++++- demo/notebooks/03_langgraph_agent.ipynb | 135 +++++++++++++++++- .../langchain_coordinode/graph.py | 17 ++- 6 files changed, 293 insertions(+), 9 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 7684958..9d62b91 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -416,8 +416,12 @@ def _build_property_definitions( "list": property_type_cls.PROPERTY_TYPE_LIST, "map": property_type_cls.PROPERTY_TYPE_MAP, } + if properties is None: + return [] + if not isinstance(properties, (list, tuple)): + raise ValueError(f"'properties' must be a list of property dicts or None; got {type(properties).__name__}") result = [] - for idx, p in enumerate(properties or []): + for idx, p in enumerate(properties): if not isinstance(p, dict): raise ValueError(f"Property at index {idx} must be a dict; got {p!r}") name = p.get("name") diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index f20fb64..e917972 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -40,7 +40,51 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"nest_asyncio\",\n ],\n check=True,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"Ready\")" + ] }, { "cell_type": "markdown", @@ -252,7 +296,9 @@ " return \"Person\"\n", " if name in tech_names:\n", " return \"Technology\"\n", - " return \"Company\"\n", + " if name in company_names:\n", + " return \"Company\"\n", + " raise ValueError(f\"Unknown edge endpoint: {name!r}\"\n", "\n", "\n", "for src, rel, dst, props in edges:\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 2189648..1c2d282 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,55 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\n# In Colab, build from source (~5 min first run, cached after).\n# Locally, it installs in seconds if a pre-built wheel is cached.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", + "# In Colab, build from source (~5 min first run, cached after).\n", + "# Locally, it installs in seconds if a pre-built wheel is cached.\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"SDK installed\")" + ] }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 599dbb0..bb52643 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,49 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n)\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " ],\n", + " check=True,\n", + ")\n", + "\n", + "print(\"SDK installed\")" + ] }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 7ff0cdc..8d1b3cf 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,50 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Always install coordinode-embedded so it's available as a local fallback.\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Security: `--proto \"=https\" --tlsv1.2` limits the connection to HTTPS with\n # TLS 1.2 minimum; rustup.rs is the official Rust Foundation installer endpoint.\n subprocess.run('curl --proto \"=https\" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y -q', shell=True, check=True)\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n)\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Always install coordinode-embedded so it's available as a local fallback.\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " \"langgraph\",\n", + " ],\n", + " check=True,\n", + ")\n", + "\n", + "print(\"SDK installed\")" + ] }, { "cell_type": "markdown", @@ -107,7 +150,95 @@ "id": "d4e5f6a7-0003-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n re.IGNORECASE,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must include $sess filtering: WHERE n.session = $sess\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not _SESSION_SCOPE_RE.search(q):\n return (\n \"Query must scope reads to the current session with either \"\n \"WHERE .session = $sess or {session: $sess}\"\n )\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" + "source": [ + "import os, re, uuid\n", + "from langchain_core.tools import tool\n", + "\n", + "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", + "\n", + "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "# Regex guards for query_facts (demo safety guard).\n", + "_WRITE_CLAUSE_RE = re.compile(\n", + " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", + " re.IGNORECASE,\n", + ")\n", + "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", + "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", + "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", + "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", + "# In production code, use server-side row-level security instead of client regex.\n", + "_SESSION_SCOPE_RE = re.compile(\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", + " re.IGNORECASE,\n", + ")\n", + "\n", + "\n", + "@tool\n", + "def save_fact(subject: str, relation: str, obj: str) -> str:\n", + " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", + " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " rel_type = relation.upper().replace(\" \", \"_\")\n", + " # Validate rel_type before interpolating into Cypher to prevent injection.\n", + " if not _REL_TYPE_RE.fullmatch(rel_type):\n", + " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", + " client.cypher(\n", + " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", + " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", + " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", + " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", + " )\n", + " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", + "\n", + "\n", + "@tool\n", + "def query_facts(cypher: str) -> str:\n", + " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", + " Must scope reads via either WHERE .session = $sess\n", + " or a node pattern {session: $sess}.\"\"\"\n", + " q = cypher.strip()\n", + " if _WRITE_CLAUSE_RE.search(q):\n", + " return \"Only read-only Cypher is allowed in query_facts.\"\n", + " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", + " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", + " if not _SESSION_SCOPE_RE.search(q):\n", + " return (\n", + " \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", + " )\n", + " rows = client.cypher(q, params={\"sess\": SESSION})\n", + " return str(rows[:20]) if rows else \"No results\"\n", + "\n", + "\n", + "@tool\n", + "def find_related(entity_name: str, depth: int = 1) -> str:\n", + " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " safe_depth = max(1, min(int(depth), 3))\n", + " rows = client.cypher(\n", + " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n", + " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", + " params={\"name\": entity_name, \"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return f\"No related entities found for {entity_name}\"\n", + " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", + "\n", + "@tool\n", + "def list_all_facts() -> str:\n", + " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", + " rows = client.cypher(\n", + " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", + " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", + " params={\"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return \"No facts stored yet\"\n", + " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "\n", + "\n", + "tools = [save_fact, query_facts, find_related, list_all_facts]\n", + "print(f\"Session: {SESSION}\")\n", + "print(\"Tools:\", [t.name for t in tools])" + ] }, { "cell_type": "markdown", diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index d0ac3dc..6fe6870 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -90,10 +90,23 @@ def refresh_schema(self) -> None: path. Injected clients (e.g. ``_EmbeddedAdapter`` in Colab notebooks) that do not expose those methods fall back to ``_parse_schema()``. Injected clients that only expose ``cypher()`` / ``close()`` (e.g. - a bare ``coordinode-embedded`` ``LocalClient``) return an empty schema. + a bare ``coordinode-embedded`` ``LocalClient``) return empty node/rel + property metadata, though relationships may still be inferred from + graph data via a Cypher query and populated in + ``structured["relationships"]``. """ get_schema_text = getattr(self._client, "get_schema_text", None) - self._schema = get_schema_text() if callable(get_schema_text) else "" + if callable(get_schema_text): + try: + self._schema = get_schema_text() + except Exception: + logger.debug( + "get_schema_text() raised — continuing with empty schema text", + exc_info=True, + ) + self._schema = "" + else: + self._schema = "" if callable(getattr(self._client, "get_labels", None)) and callable( getattr(self._client, "get_edge_types", None) From f911bf97d2e25da0fb4d367784a364be472922a0 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 04:47:32 +0300 Subject: [PATCH 24/86] fix(notebooks): syntax error in _label(), remove vector_search stub from adapters - Fix missing closing ) in raise ValueError in 00_seed_data.ipynb _label() (SyntaxError prevented relationship inserts from running) - Remove vector_search() stub from _EmbeddedAdapter in notebooks 01 and 02: callable(getattr(client, "vector_search", None)) must return falsy so embedded mode correctly reports supports_vector_queries=False - Add real-server warning to 00_seed_data.ipynb intro markdown --- demo/notebooks/00_seed_data.ipynb | 3979 ++++++++++++++++- .../01_llama_index_property_graph.ipynb | 1115 ++++- demo/notebooks/02_langchain_graph_chain.ipynb | 1115 ++++- 3 files changed, 6072 insertions(+), 137 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index e917972..89df3ce 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -5,25 +5,1130 @@ "id": "a1b2c3d4-0000-0000-0000-000000000001", "metadata": {}, "source": [ - "# Seed Demo Data\n", + "#", + " ", + "S", + "e", + "e", + "d", + " ", + "D", + "e", + "m", + "o", + " ", + "D", + "a", + "t", + "a", "\n", - "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb)\n", "\n", - "Populates CoordiNode with a **tech industry knowledge graph** you can explore\n", - "in notebooks 01–03.\n", + "[", + "!", + "[", + "O", + "p", + "e", + "n", + " ", + "i", + "n", + " ", + "C", + "o", + "l", + "a", + "b", + "]", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "c", + "o", + "l", + "a", + "b", + ".", + "r", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + ".", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "m", + "/", + "a", + "s", + "s", + "e", + "t", + "s", + "/", + "c", + "o", + "l", + "a", + "b", + "-", + "b", + "a", + "d", + "g", + "e", + ".", + "s", + "v", + "g", + ")", + "]", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "c", + "o", + "l", + "a", + "b", + ".", + "r", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + ".", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "m", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + "/", + "b", + "l", + "o", + "b", + "/", + "m", + "a", + "i", + "n", + "/", + "d", + "e", + "m", + "o", + "/", + "n", + "o", + "t", + "e", + "b", + "o", + "o", + "k", + "s", + "/", + "0", + "0", + "_", + "s", + "e", + "e", + "d", + "_", + "d", + "a", + "t", + "a", + ".", + "i", + "p", + "y", + "n", + "b", + ")", "\n", - "**Graph contents:**\n", - "- 10 people (engineers, researchers, founders)\n", - "- 6 companies\n", - "- 8 technologies / research areas\n", - "- ~35 relationships (WORKS_AT, FOUNDED, KNOWS, RESEARCHES, INVENTED, ACQUIRED, USES, …)\n", "\n", - "All nodes carry a `demo=true` property so they can be bulk-deleted without\n", - "touching other data.\n", + "P", + "o", + "p", + "u", + "l", + "a", + "t", + "e", + "s", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "w", + "i", + "t", + "h", + " ", + "a", + " ", + "*", + "*", + "t", + "e", + "c", + "h", + " ", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + " ", + "k", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "g", + "r", + "a", + "p", + "h", + "*", + "*", + " ", + "y", + "o", + "u", + " ", + "c", + "a", + "n", + " ", + "e", + "x", + "p", + "l", + "o", + "r", + "e", "\n", - "**Environments:**\n", - "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", - "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC." + "i", + "n", + " ", + "n", + "o", + "t", + "e", + "b", + "o", + "o", + "k", + "s", + " ", + "0", + "1", + "–", + "0", + "3", + ".", + "\n", + "\n", + "*", + "*", + "G", + "r", + "a", + "p", + "h", + " ", + "c", + "o", + "n", + "t", + "e", + "n", + "t", + "s", + ":", + "*", + "*", + "\n", + "-", + " ", + "1", + "0", + " ", + "p", + "e", + "o", + "p", + "l", + "e", + " ", + "(", + "e", + "n", + "g", + "i", + "n", + "e", + "e", + "r", + "s", + ",", + " ", + "r", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "e", + "r", + "s", + ",", + " ", + "f", + "o", + "u", + "n", + "d", + "e", + "r", + "s", + ")", + "\n", + "-", + " ", + "6", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + "\n", + "-", + " ", + "8", + " ", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + " ", + "/", + " ", + "r", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + " ", + "a", + "r", + "e", + "a", + "s", + "\n", + "-", + " ", + "~", + "3", + "5", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + "s", + "h", + "i", + "p", + "s", + " ", + "(", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + ",", + " ", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + ",", + " ", + "K", + "N", + "O", + "W", + "S", + ",", + " ", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + ",", + " ", + "I", + "N", + "V", + "E", + "N", + "T", + "E", + "D", + ",", + " ", + "A", + "C", + "Q", + "U", + "I", + "R", + "E", + "D", + ",", + " ", + "U", + "S", + "E", + "S", + ",", + " ", + "…", + ")", + "\n", + "\n", + "A", + "l", + "l", + " ", + "n", + "o", + "d", + "e", + "s", + " ", + "c", + "a", + "r", + "r", + "y", + " ", + "a", + " ", + "`", + "d", + "e", + "m", + "o", + "=", + "t", + "r", + "u", + "e", + "`", + " ", + "p", + "r", + "o", + "p", + "e", + "r", + "t", + "y", + " ", + "s", + "o", + " ", + "t", + "h", + "e", + "y", + " ", + "c", + "a", + "n", + " ", + "b", + "e", + " ", + "b", + "u", + "l", + "k", + "-", + "d", + "e", + "l", + "e", + "t", + "e", + "d", + " ", + "w", + "i", + "t", + "h", + "o", + "u", + "t", + "\n", + "t", + "o", + "u", + "c", + "h", + "i", + "n", + "g", + " ", + "o", + "t", + "h", + "e", + "r", + " ", + "d", + "a", + "t", + "a", + ".", + "\n", + "\n", + "*", + "*", + "E", + "n", + "v", + "i", + "r", + "o", + "n", + "m", + "e", + "n", + "t", + "s", + ":", + "*", + "*", + "\n", + "-", + " ", + "*", + "*", + "G", + "o", + "o", + "g", + "l", + "e", + " ", + "C", + "o", + "l", + "a", + "b", + "*", + "*", + " ", + "—", + " ", + "u", + "s", + "e", + "s", + " ", + "`", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "`", + " ", + "(", + "i", + "n", + "-", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "e", + "n", + "g", + "i", + "n", + "e", + ",", + " ", + "n", + "o", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "n", + "e", + "e", + "d", + "e", + "d", + ")", + ".", + " ", + "F", + "i", + "r", + "s", + "t", + " ", + "r", + "u", + "n", + " ", + "c", + "o", + "m", + "p", + "i", + "l", + "e", + "s", + " ", + "f", + "r", + "o", + "m", + " ", + "s", + "o", + "u", + "r", + "c", + "e", + " ", + "(", + "~", + "5", + " ", + "m", + "i", + "n", + ")", + ";", + " ", + "s", + "u", + "b", + "s", + "e", + "q", + "u", + "e", + "n", + "t", + " ", + "r", + "u", + "n", + "s", + " ", + "u", + "s", + "e", + " ", + "t", + "h", + "e", + " ", + "p", + "i", + "p", + " ", + "c", + "a", + "c", + "h", + "e", + ".", + "\n", + "-", + " ", + "*", + "*", + "L", + "o", + "c", + "a", + "l", + " ", + "/", + " ", + "D", + "o", + "c", + "k", + "e", + "r", + " ", + "C", + "o", + "m", + "p", + "o", + "s", + "e", + "*", + "*", + " ", + "—", + " ", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "s", + " ", + "t", + "o", + " ", + "a", + " ", + "r", + "u", + "n", + "n", + "i", + "n", + "g", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "v", + "i", + "a", + " ", + "g", + "R", + "P", + "C", + ".", + "\n", + "\n", + ">", + " ", + "*", + "*", + "⚠", + "️", + " ", + "N", + "o", + "t", + "e", + " ", + "f", + "o", + "r", + " ", + "r", + "e", + "a", + "l", + "-", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "u", + "s", + "e", + ":", + "*", + "*", + " ", + "M", + "E", + "R", + "G", + "E", + " ", + "o", + "p", + "e", + "r", + "a", + "t", + "i", + "o", + "n", + "s", + " ", + "m", + "a", + "t", + "c", + "h", + " ", + "n", + "o", + "d", + "e", + "s", + " ", + "b", + "y", + " ", + "n", + "a", + "m", + "e", + " ", + "(", + "e", + ".", + "g", + ".", + " ", + "`", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + "`", + ")", + ".", + " ", + "R", + "u", + "n", + " ", + "t", + "h", + "i", + "s", + " ", + "n", + "o", + "t", + "e", + "b", + "o", + "o", + "k", + " ", + "a", + "g", + "a", + "i", + "n", + "s", + "t", + " ", + "a", + " ", + "f", + "r", + "e", + "s", + "h", + "/", + "e", + "m", + "p", + "t", + "y", + " ", + "d", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + " ", + "o", + "r", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + " ", + "t", + "o", + " ", + "a", + "v", + "o", + "i", + "d", + " ", + "a", + "c", + "c", + "i", + "d", + "e", + "n", + "t", + "a", + "l", + "l", + "y", + " ", + "t", + "a", + "g", + "g", + "i", + "n", + "g", + " ", + "a", + "n", + "d", + " ", + "t", + "h", + "e", + "n", + " ", + "d", + "e", + "l", + "e", + "t", + "i", + "n", + "g", + " ", + "p", + "r", + "e", + "-", + "e", + "x", + "i", + "s", + "t", + "i", + "n", + "g", + " ", + "n", + "o", + "d", + "e", + "s", + " ", + "d", + "u", + "r", + "i", + "n", + "g", + " ", + "c", + "l", + "e", + "a", + "n", + "u", + "p", + "." ] }, { @@ -242,78 +1347,2790 @@ "metadata": {}, "outputs": [], "source": [ - "edges = [\n", - " # WORKS_AT\n", - " (\"Alice Chen\", \"WORKS_AT\", \"DeepMind\", {}),\n", - " (\"Bob Torres\", \"WORKS_AT\", \"Google\", {}),\n", - " (\"Carol Smith\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", - " (\"David Park\", \"WORKS_AT\", \"OpenAI\", {}),\n", - " (\"Eva Müller\", \"WORKS_AT\", \"Synthex\", {\"since\": 2022}),\n", - " (\"Frank Liu\", \"WORKS_AT\", \"Meta\", {}),\n", - " (\"Grace Okafor\", \"WORKS_AT\", \"MIT\", {}),\n", - " (\"Henry Rossi\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", - " (\"Isla Nakamura\", \"WORKS_AT\", \"DeepMind\", {}),\n", - " (\"James Wright\", \"WORKS_AT\", \"Google\", {}),\n", - " # FOUNDED\n", - " (\"Carol Smith\", \"FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", - " (\"Henry Rossi\", \"CO_FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", - " # KNOWS\n", - " (\"Alice Chen\", \"KNOWS\", \"Isla Nakamura\", {}),\n", - " (\"Alice Chen\", \"KNOWS\", \"David Park\", {}),\n", - " (\"Carol Smith\", \"KNOWS\", \"Bob Torres\", {}),\n", - " (\"Grace Okafor\", \"KNOWS\", \"Alice Chen\", {}),\n", - " (\"Frank Liu\", \"KNOWS\", \"James Wright\", {}),\n", - " (\"Eva Müller\", \"KNOWS\", \"Grace Okafor\", {}),\n", - " # RESEARCHES / WORKS_ON\n", - " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", - " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", - " (\"Grace Okafor\", \"RESEARCHES\", \"Knowledge Graph\", {\"since\": 2021}),\n", - " (\"Isla Nakamura\", \"RESEARCHES\", \"Graph Neural Network\", {\"since\": 2020}),\n", - " (\"Frank Liu\", \"RESEARCHES\", \"Graph Neural Network\", {}),\n", - " (\"Grace Okafor\", \"RESEARCHES\", \"GraphRAG\", {\"since\": 2023}),\n", - " # USES\n", - " (\"Synthex\", \"USES\", \"Knowledge Graph\", {}),\n", - " (\"Synthex\", \"USES\", \"Vector Database\", {}),\n", - " (\"Synthex\", \"USES\", \"RAG\", {}),\n", - " (\"OpenAI\", \"USES\", \"Transformer\", {}),\n", - " (\"Google\", \"USES\", \"Transformer\", {}),\n", - " # ACQUIRED\n", - " (\"Google\", \"ACQUIRED\", \"DeepMind\", {\"year\": 2014}),\n", - " # BUILDS_ON\n", - " (\"GraphRAG\", \"BUILDS_ON\", \"Knowledge Graph\", {}),\n", - " (\"GraphRAG\", \"BUILDS_ON\", \"RAG\", {}),\n", - " (\"RAG\", \"BUILDS_ON\", \"Vector Database\", {}),\n", - " (\"LLM\", \"BUILDS_ON\", \"Transformer\", {}),\n", - "]\n", + "e", + "d", + "g", + "e", + "s", + " ", + "=", + " ", + "[", "\n", - "src_names = {p[\"name\"] for p in people}\n", - "tech_names = {t[\"name\"] for t in technologies}\n", - "company_names = {c[\"name\"] for c in companies}\n", + " ", + " ", + " ", + " ", + "#", + " ", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", "\n", - "def _label(name):\n", - " if name in src_names:\n", - " return \"Person\"\n", - " if name in tech_names:\n", - " return \"Technology\"\n", - " if name in company_names:\n", - " return \"Company\"\n", - " raise ValueError(f\"Unknown edge endpoint: {name!r}\"\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "B", + "o", + "b", + " ", + "T", + "o", + "r", + "r", + "e", + "s", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "E", + "v", + "a", + " ", + "M", + "ü", + "l", + "l", + "e", + "r", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "2", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "M", + "e", + "t", + "a", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "M", + "I", + "T", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "H", + "e", + "n", + "r", + "y", + " ", + "R", + "o", + "s", + "s", + "i", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "J", + "a", + "m", + "e", + "s", + " ", + "W", + "r", + "i", + "g", + "h", + "t", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "H", + "e", + "n", + "r", + "y", + " ", + "R", + "o", + "s", + "s", + "i", + "\"", + ",", + " ", + "\"", + "C", + "O", + "_", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "K", + "N", + "O", + "W", + "S", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "B", + "o", + "b", + " ", + "T", + "o", + "r", + "r", + "e", + "s", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "J", + "a", + "m", + "e", + "s", + " ", + "W", + "r", + "i", + "g", + "h", + "t", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "E", + "v", + "a", + " ", + "M", + "ü", + "l", + "l", + "e", + "r", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + " ", + "/", + " ", + "W", + "O", + "R", + "K", + "S", + "_", + "O", + "N", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "R", + "e", + "i", + "n", + "f", + "o", + "r", + "c", + "e", + "m", + "e", + "n", + "t", + " ", + "L", + "e", + "a", + "r", + "n", + "i", + "n", + "g", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "1", + "9", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "L", + "L", + "M", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "0", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "0", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "3", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "U", + "S", + "E", + "S", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "C", + "Q", + "U", + "I", + "R", + "E", + "D", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "A", + "C", + "Q", + "U", + "I", + "R", + "E", + "D", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "4", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "L", + "L", + "M", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + "]", + "\n", + "\n", + "s", + "r", + "c", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "p", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "p", + " ", + "i", + "n", + " ", + "p", + "e", + "o", + "p", + "l", + "e", + "}", + "\n", + "t", + "e", + "c", + "h", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "t", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "t", + " ", + "i", + "n", + " ", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + "}", + "\n", + "c", + "o", + "m", + "p", + "a", + "n", + "y", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "c", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "c", + " ", + "i", + "n", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + "}", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "n", + "a", + "m", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "s", + "r", + "c", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "P", + "e", + "r", + "s", + "o", + "n", + "\"", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "t", + "e", + "c", + "h", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "y", + "\"", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "y", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "C", + "o", + "m", + "p", + "a", + "n", + "y", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "a", + "i", + "s", + "e", + " ", + "V", + "a", + "l", + "u", + "e", + "E", + "r", + "r", + "o", + "r", + "(", + "f", + "\"", + "U", + "n", + "k", + "n", + "o", + "w", + "n", + " ", + "e", + "d", + "g", + "e", + " ", + "e", + "n", + "d", + "p", + "o", + "i", + "n", + "t", + ":", + " ", + "{", + "n", + "a", + "m", + "e", + "!", + "r", + "}", + "\"", + ")", + "\n", + "\n", + "\n", + "f", + "o", + "r", + " ", + "s", + "r", + "c", + ",", + " ", + "r", + "e", + "l", + ",", + " ", + "d", + "s", + "t", + ",", + " ", + "p", + "r", + "o", + "p", + "s", + " ", + "i", + "n", + " ", + "e", + "d", + "g", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + "s", + "r", + "c", + "_", + "l", + "a", + "b", + "e", + "l", + " ", + "=", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "s", + "r", + "c", + ")", + "\n", + " ", + " ", + " ", + " ", + "d", + "s", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + " ", + "=", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "d", + "s", + "t", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + " ", + "=", + " ", + "\"", + ",", + " ", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "f", + "\"", + "r", + ".", + "{", + "k", + "}", + " ", + "=", + " ", + "$", + "{", + "k", + "}", + "\"", + " ", + "f", + "o", + "r", + " ", + "k", + " ", + "i", + "n", + " ", + "p", + "r", + "o", + "p", + "s", + ")", + " ", + "i", + "f", + " ", + "p", + "r", + "o", + "p", + "s", + " ", + "e", + "l", + "s", + "e", + " ", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "e", + "t", + "_", + "p", + "a", + "r", + "t", + " ", + "=", + " ", + "f", + "\"", + " ", + "S", + "E", + "T", + " ", + "{", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + "}", + "\"", + " ", + "i", + "f", + " ", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + " ", + "e", + "l", + "s", + "e", + " ", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "a", + ":", + "{", + "s", + "r", + "c", + "_", + "l", + "a", + "b", + "e", + "l", + "}", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "s", + "r", + "c", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "b", + ":", + "{", + "d", + "s", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + "}", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "d", + "s", + "t", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "a", + ")", + "-", + "[", + "r", + ":", + "{", + "r", + "e", + "l", + "}", + "]", + "-", + ">", + "(", + "b", + ")", + "\"", + " ", + "+", + " ", + "s", + "e", + "t", + "_", + "p", + "a", + "r", + "t", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "s", + "r", + "c", + "\"", + ":", + " ", + "s", + "r", + "c", + ",", + " ", + "\"", + "d", + "s", + "t", + "\"", + ":", + " ", + "d", + "s", + "t", + ",", + " ", + "*", + "*", + "p", + "r", + "o", + "p", + "s", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", "\n", - "for src, rel, dst, props in edges:\n", - " src_label = _label(src)\n", - " dst_label = _label(dst)\n", - " set_clause = \", \".join(f\"r.{k} = ${k}\" for k in props) if props else \"\"\n", - " set_part = f\" SET {set_clause}\" if set_clause else \"\"\n", - " client.cypher(\n", - " f\"MATCH (a:{src_label} {{name: $src}}) \"\n", - " f\"MATCH (b:{dst_label} {{name: $dst}}) \"\n", - " f\"MERGE (a)-[r:{rel}]->(b)\" + set_part,\n", - " params={\"src\": src, \"dst\": dst, **props},\n", - " )\n", "\n", - "print(f\"Created {len(edges)} relationships\")" + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "r", + "e", + "a", + "t", + "e", + "d", + " ", + "{", + "l", + "e", + "n", + "(", + "e", + "d", + "g", + "e", + "s", + ")", + "}", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + "s", + "h", + "i", + "p", + "s", + "\"", + ")" ] }, { @@ -393,4 +4210,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 1c2d282..23689b2 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -108,42 +108,1101 @@ "metadata": {}, "outputs": [], "source": [ - "class _EmbeddedAdapter:\n", - " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + "c", + "l", + "a", + "s", + "s", + " ", + "_", + "E", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "A", + "d", + "a", + "p", + "t", + "e", + "r", + ":", "\n", - " def __init__(self, local_client):\n", - " self._lc = local_client\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "T", + "h", + "i", + "n", + " ", + "w", + "r", + "a", + "p", + "p", + "e", + "r", + " ", + "a", + "r", + "o", + "u", + "n", + "d", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + " ", + "t", + "h", + "a", + "t", + " ", + "a", + "d", + "d", + "s", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "-", + "c", + "o", + "m", + "p", + "a", + "t", + "i", + "b", + "l", + "e", + " ", + "m", + "e", + "t", + "h", + "o", + "d", + "s", + ".", + "\"", + "\"", + "\"", "\n", - " def cypher(self, query, params=None):\n", - " return self._lc.cypher(query, params or {})\n", "\n", - " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", - " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", - " lines = [\"Node labels:\"]\n", - " for r in lbls:\n", - " lines.append(f\" - {r['lbl']}\")\n", - " lines.append(\"\\nEdge types:\")\n", - " for r in rels:\n", - " lines.append(f\" - {r['t']}\")\n", - " return \"\\n\".join(lines)\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "_", + "_", + "i", + "n", + "i", + "t", + "_", + "_", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "l", + "o", + "c", + "a", + "l", + "_", + "c", + "l", + "i", + "e", + "n", + "t", + ")", + ":", "\n", - " def vector_search(self, **kwargs):\n", - " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + " ", + "=", + " ", + "l", + "o", + "c", + "a", + "l", + "_", + "c", + "l", + "i", + "e", + "n", + "t", "\n", - " return []\n", "\n", - " def close(self):\n", - " self._lc.close()\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "q", + "u", + "e", + "r", + "y", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "N", + "o", + "n", + "e", + ")", + ":", "\n", - " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "q", + "u", + "e", + "r", + "y", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + " ", + "o", + "r", + " ", + "{", + "}", + ")", "\n", - " return []\n", "\n", - " def get_edge_types(self):\n", - " # Schema introspection not available in embedded mode.\n", - " return []" + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "s", + "c", + "h", + "e", + "m", + "a", + "_", + "t", + "e", + "x", + "t", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "b", + "l", + "s", + " ", + "=", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + ")", + " ", + "U", + "N", + "W", + "I", + "N", + "D", + " ", + "l", + "a", + "b", + "e", + "l", + "s", + "(", + "n", + ")", + " ", + "A", + "S", + " ", + "l", + "b", + "l", + " ", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "D", + "I", + "S", + "T", + "I", + "N", + "C", + "T", + " ", + "l", + "b", + "l", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "l", + "b", + "l", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "l", + "s", + " ", + "=", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + ")", + "-", + "[", + "r", + "]", + "-", + ">", + "(", + ")", + " ", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "D", + "I", + "S", + "T", + "I", + "N", + "C", + "T", + " ", + "t", + "y", + "p", + "e", + "(", + "r", + ")", + " ", + "A", + "S", + " ", + "t", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "t", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + " ", + "=", + " ", + "[", + "\"", + "N", + "o", + "d", + "e", + " ", + "l", + "a", + "b", + "e", + "l", + "s", + ":", + "\"", + "]", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "l", + "b", + "l", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "f", + "\"", + " ", + " ", + "-", + " ", + "{", + "r", + "[", + "'", + "l", + "b", + "l", + "'", + "]", + "}", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "\"", + "\\", + "n", + "E", + "d", + "g", + "e", + " ", + "t", + "y", + "p", + "e", + "s", + ":", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "r", + "e", + "l", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "f", + "\"", + " ", + " ", + "-", + " ", + "{", + "r", + "[", + "'", + "t", + "'", + "]", + "}", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "\\", + "n", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "l", + "i", + "n", + "e", + "s", + ")", + "\n", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "s", + "e", + "a", + "r", + "c", + "h", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + " ", + "—", + " ", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "r", + "u", + "n", + "n", + "i", + "n", + "g", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + ".", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "c", + "l", + "o", + "s", + "e", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + "s", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "#", + " ", + "S", + "c", + "h", + "e", + "m", + "a", + " ", + "i", + "n", + "t", + "r", + "o", + "s", + "p", + "e", + "c", + "t", + "i", + "o", + "n", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + ".", + "\n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "e", + "d", + "g", + "e", + "_", + "t", + "y", + "p", + "e", + "s", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "#", + " ", + "S", + "c", + "h", + "e", + "m", + "a", + " ", + "i", + "n", + "t", + "r", + "o", + "s", + "p", + "e", + "c", + "t", + "i", + "o", + "n", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]" ] }, { @@ -403,4 +1462,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index bb52643..cf05d53 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -99,42 +99,1101 @@ "metadata": {}, "outputs": [], "source": [ - "class _EmbeddedAdapter:\n", - " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", + "c", + "l", + "a", + "s", + "s", + " ", + "_", + "E", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "A", + "d", + "a", + "p", + "t", + "e", + "r", + ":", "\n", - " def __init__(self, local_client):\n", - " self._lc = local_client\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "T", + "h", + "i", + "n", + " ", + "w", + "r", + "a", + "p", + "p", + "e", + "r", + " ", + "a", + "r", + "o", + "u", + "n", + "d", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + " ", + "t", + "h", + "a", + "t", + " ", + "a", + "d", + "d", + "s", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "-", + "c", + "o", + "m", + "p", + "a", + "t", + "i", + "b", + "l", + "e", + " ", + "m", + "e", + "t", + "h", + "o", + "d", + "s", + ".", + "\"", + "\"", + "\"", "\n", - " def cypher(self, query, params=None):\n", - " return self._lc.cypher(query, params or {})\n", "\n", - " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", - " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", - " lines = [\"Node labels:\"]\n", - " for r in lbls:\n", - " lines.append(f\" - {r['lbl']}\")\n", - " lines.append(\"\\nEdge types:\")\n", - " for r in rels:\n", - " lines.append(f\" - {r['t']}\")\n", - " return \"\\n\".join(lines)\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "_", + "_", + "i", + "n", + "i", + "t", + "_", + "_", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "l", + "o", + "c", + "a", + "l", + "_", + "c", + "l", + "i", + "e", + "n", + "t", + ")", + ":", "\n", - " def vector_search(self, **kwargs):\n", - " # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + " ", + "=", + " ", + "l", + "o", + "c", + "a", + "l", + "_", + "c", + "l", + "i", + "e", + "n", + "t", "\n", - " return []\n", "\n", - " def close(self):\n", - " self._lc.close()\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "s", + "e", + "l", + "f", + ",", + " ", + "q", + "u", + "e", + "r", + "y", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "N", + "o", + "n", + "e", + ")", + ":", "\n", - " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "q", + "u", + "e", + "r", + "y", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + " ", + "o", + "r", + " ", + "{", + "}", + ")", "\n", - " return []\n", "\n", - " def get_edge_types(self):\n", - " # Schema introspection not available in embedded mode.\n", - " return []" + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "s", + "c", + "h", + "e", + "m", + "a", + "_", + "t", + "e", + "x", + "t", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "b", + "l", + "s", + " ", + "=", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + ")", + " ", + "U", + "N", + "W", + "I", + "N", + "D", + " ", + "l", + "a", + "b", + "e", + "l", + "s", + "(", + "n", + ")", + " ", + "A", + "S", + " ", + "l", + "b", + "l", + " ", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "D", + "I", + "S", + "T", + "I", + "N", + "C", + "T", + " ", + "l", + "b", + "l", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "l", + "b", + "l", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "l", + "s", + " ", + "=", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + ")", + "-", + "[", + "r", + "]", + "-", + ">", + "(", + ")", + " ", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "D", + "I", + "S", + "T", + "I", + "N", + "C", + "T", + " ", + "t", + "y", + "p", + "e", + "(", + "r", + ")", + " ", + "A", + "S", + " ", + "t", + " ", + "O", + "R", + "D", + "E", + "R", + " ", + "B", + "Y", + " ", + "t", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + " ", + "=", + " ", + "[", + "\"", + "N", + "o", + "d", + "e", + " ", + "l", + "a", + "b", + "e", + "l", + "s", + ":", + "\"", + "]", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "l", + "b", + "l", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "f", + "\"", + " ", + " ", + "-", + " ", + "{", + "r", + "[", + "'", + "l", + "b", + "l", + "'", + "]", + "}", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "\"", + "\\", + "n", + "E", + "d", + "g", + "e", + " ", + "t", + "y", + "p", + "e", + "s", + ":", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "r", + "e", + "l", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "l", + "i", + "n", + "e", + "s", + ".", + "a", + "p", + "p", + "e", + "n", + "d", + "(", + "f", + "\"", + " ", + " ", + "-", + " ", + "{", + "r", + "[", + "'", + "t", + "'", + "]", + "}", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "\\", + "n", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "l", + "i", + "n", + "e", + "s", + ")", + "\n", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "s", + "e", + "a", + "r", + "c", + "h", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + " ", + "—", + " ", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "r", + "u", + "n", + "n", + "i", + "n", + "g", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + ".", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "c", + "l", + "o", + "s", + "e", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "e", + "l", + "f", + ".", + "_", + "l", + "c", + ".", + "c", + "l", + "o", + "s", + "e", + "(", + ")", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + "s", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "#", + " ", + "S", + "c", + "h", + "e", + "m", + "a", + " ", + "i", + "n", + "t", + "r", + "o", + "s", + "p", + "e", + "c", + "t", + "i", + "o", + "n", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + ".", + "\n", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]", + "\n", + "\n", + " ", + " ", + " ", + " ", + "d", + "e", + "f", + " ", + "g", + "e", + "t", + "_", + "e", + "d", + "g", + "e", + "_", + "t", + "y", + "p", + "e", + "s", + "(", + "s", + "e", + "l", + "f", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "#", + " ", + "S", + "c", + "h", + "e", + "m", + "a", + " ", + "i", + "n", + "t", + "r", + "o", + "s", + "p", + "e", + "c", + "t", + "i", + "o", + "n", + " ", + "n", + "o", + "t", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "i", + "n", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "m", + "o", + "d", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "[", + "]" ] }, { @@ -390,4 +1449,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From ee969e20826d23412e4ee4776d9be848e55fe487 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 04:49:10 +0300 Subject: [PATCH 25/86] style: ruff format notebooks after vector_search stub removal --- demo/notebooks/00_seed_data.ipynb | 2845 +---------------- .../01_llama_index_property_graph.ipynb | 1113 +------ demo/notebooks/02_langchain_graph_chain.ipynb | 1113 +------ demo/notebooks/03_langgraph_agent.ipynb | 5 +- 4 files changed, 121 insertions(+), 4955 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 89df3ce..88c6d6d 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -1159,6 +1159,7 @@ " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", " _ctx = _ssl.create_default_context()\n", " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", @@ -1347,2790 +1348,78 @@ "metadata": {}, "outputs": [], "source": [ - "e", - "d", - "g", - "e", - "s", - " ", - "=", - " ", - "[", + "edges = [\n", + " # WORKS_AT\n", + " (\"Alice Chen\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"Bob Torres\", \"WORKS_AT\", \"Google\", {}),\n", + " (\"Carol Smith\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"David Park\", \"WORKS_AT\", \"OpenAI\", {}),\n", + " (\"Eva Müller\", \"WORKS_AT\", \"Synthex\", {\"since\": 2022}),\n", + " (\"Frank Liu\", \"WORKS_AT\", \"Meta\", {}),\n", + " (\"Grace Okafor\", \"WORKS_AT\", \"MIT\", {}),\n", + " (\"Henry Rossi\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"James Wright\", \"WORKS_AT\", \"Google\", {}),\n", + " # FOUNDED\n", + " (\"Carol Smith\", \"FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", + " (\"Henry Rossi\", \"CO_FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", + " # KNOWS\n", + " (\"Alice Chen\", \"KNOWS\", \"Isla Nakamura\", {}),\n", + " (\"Alice Chen\", \"KNOWS\", \"David Park\", {}),\n", + " (\"Carol Smith\", \"KNOWS\", \"Bob Torres\", {}),\n", + " (\"Grace Okafor\", \"KNOWS\", \"Alice Chen\", {}),\n", + " (\"Frank Liu\", \"KNOWS\", \"James Wright\", {}),\n", + " (\"Eva Müller\", \"KNOWS\", \"Grace Okafor\", {}),\n", + " # RESEARCHES / WORKS_ON\n", + " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", + " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"Knowledge Graph\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"RESEARCHES\", \"Graph Neural Network\", {\"since\": 2020}),\n", + " (\"Frank Liu\", \"RESEARCHES\", \"Graph Neural Network\", {}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"GraphRAG\", {\"since\": 2023}),\n", + " # USES\n", + " (\"Synthex\", \"USES\", \"Knowledge Graph\", {}),\n", + " (\"Synthex\", \"USES\", \"Vector Database\", {}),\n", + " (\"Synthex\", \"USES\", \"RAG\", {}),\n", + " (\"OpenAI\", \"USES\", \"Transformer\", {}),\n", + " (\"Google\", \"USES\", \"Transformer\", {}),\n", + " # ACQUIRED\n", + " (\"Google\", \"ACQUIRED\", \"DeepMind\", {\"year\": 2014}),\n", + " # BUILDS_ON\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"Knowledge Graph\", {}),\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"RAG\", {}),\n", + " (\"RAG\", \"BUILDS_ON\", \"Vector Database\", {}),\n", + " (\"LLM\", \"BUILDS_ON\", \"Transformer\", {}),\n", + "]\n", "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", + "src_names = {p[\"name\"] for p in people}\n", + "tech_names = {t[\"name\"] for t in technologies}\n", + "company_names = {c[\"name\"] for c in companies}\n", "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "B", - "o", - "b", - " ", - "T", - "o", - "r", - "r", - "e", - "s", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", + "def _label(name):\n", + " if name in src_names:\n", + " return \"Person\"\n", + " if name in tech_names:\n", + " return \"Technology\"\n", + " if name in company_names:\n", + " return \"Company\"\n", + " raise ValueError(f\"Unknown edge endpoint: {name!r}\")\n", "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", + "for src, rel, dst, props in edges:\n", + " src_label = _label(src)\n", + " dst_label = _label(dst)\n", + " set_clause = \", \".join(f\"r.{k} = ${k}\" for k in props) if props else \"\"\n", + " set_part = f\" SET {set_clause}\" if set_clause else \"\"\n", + " client.cypher(\n", + " f\"MATCH (a:{src_label} {{name: $src}}) \"\n", + " f\"MATCH (b:{dst_label} {{name: $dst}}) \"\n", + " f\"MERGE (a)-[r:{rel}]->(b)\" + set_part,\n", + " params={\"src\": src, \"dst\": dst, **props},\n", + " )\n", "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "E", - "v", - "a", - " ", - "M", - "ü", - "l", - "l", - "e", - "r", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "2", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "M", - "e", - "t", - "a", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "M", - "I", - "T", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "H", - "e", - "n", - "r", - "y", - " ", - "R", - "o", - "s", - "s", - "i", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "J", - "a", - "m", - "e", - "s", - " ", - "W", - "r", - "i", - "g", - "h", - "t", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "H", - "e", - "n", - "r", - "y", - " ", - "R", - "o", - "s", - "s", - "i", - "\"", - ",", - " ", - "\"", - "C", - "O", - "_", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "K", - "N", - "O", - "W", - "S", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "B", - "o", - "b", - " ", - "T", - "o", - "r", - "r", - "e", - "s", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "J", - "a", - "m", - "e", - "s", - " ", - "W", - "r", - "i", - "g", - "h", - "t", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "E", - "v", - "a", - " ", - "M", - "ü", - "l", - "l", - "e", - "r", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - " ", - "/", - " ", - "W", - "O", - "R", - "K", - "S", - "_", - "O", - "N", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "R", - "e", - "i", - "n", - "f", - "o", - "r", - "c", - "e", - "m", - "e", - "n", - "t", - " ", - "L", - "e", - "a", - "r", - "n", - "i", - "n", - "g", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "1", - "9", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "L", - "L", - "M", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "0", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "0", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "3", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "U", - "S", - "E", - "S", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "C", - "Q", - "U", - "I", - "R", - "E", - "D", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "A", - "C", - "Q", - "U", - "I", - "R", - "E", - "D", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "4", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "L", - "L", - "M", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - "]", - "\n", - "\n", - "s", - "r", - "c", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "p", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "p", - " ", - "i", - "n", - " ", - "p", - "e", - "o", - "p", - "l", - "e", - "}", - "\n", - "t", - "e", - "c", - "h", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "t", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - "}", - "\n", - "c", - "o", - "m", - "p", - "a", - "n", - "y", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "c", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "c", - " ", - "i", - "n", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - "}", - "\n", - "\n", - "\n", - "d", - "e", - "f", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "n", - "a", - "m", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "s", - "r", - "c", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "P", - "e", - "r", - "s", - "o", - "n", - "\"", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "t", - "e", - "c", - "h", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "y", - "\"", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "y", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "C", - "o", - "m", - "p", - "a", - "n", - "y", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "a", - "i", - "s", - "e", - " ", - "V", - "a", - "l", - "u", - "e", - "E", - "r", - "r", - "o", - "r", - "(", - "f", - "\"", - "U", - "n", - "k", - "n", - "o", - "w", - "n", - " ", - "e", - "d", - "g", - "e", - " ", - "e", - "n", - "d", - "p", - "o", - "i", - "n", - "t", - ":", - " ", - "{", - "n", - "a", - "m", - "e", - "!", - "r", - "}", - "\"", - ")", - "\n", - "\n", - "\n", - "f", - "o", - "r", - " ", - "s", - "r", - "c", - ",", - " ", - "r", - "e", - "l", - ",", - " ", - "d", - "s", - "t", - ",", - " ", - "p", - "r", - "o", - "p", - "s", - " ", - "i", - "n", - " ", - "e", - "d", - "g", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - "s", - "r", - "c", - "_", - "l", - "a", - "b", - "e", - "l", - " ", - "=", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "s", - "r", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - "d", - "s", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - " ", - "=", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "d", - "s", - "t", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - " ", - "=", - " ", - "\"", - ",", - " ", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "f", - "\"", - "r", - ".", - "{", - "k", - "}", - " ", - "=", - " ", - "$", - "{", - "k", - "}", - "\"", - " ", - "f", - "o", - "r", - " ", - "k", - " ", - "i", - "n", - " ", - "p", - "r", - "o", - "p", - "s", - ")", - " ", - "i", - "f", - " ", - "p", - "r", - "o", - "p", - "s", - " ", - "e", - "l", - "s", - "e", - " ", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "e", - "t", - "_", - "p", - "a", - "r", - "t", - " ", - "=", - " ", - "f", - "\"", - " ", - "S", - "E", - "T", - " ", - "{", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - "}", - "\"", - " ", - "i", - "f", - " ", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - " ", - "e", - "l", - "s", - "e", - " ", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "a", - ":", - "{", - "s", - "r", - "c", - "_", - "l", - "a", - "b", - "e", - "l", - "}", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "s", - "r", - "c", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "b", - ":", - "{", - "d", - "s", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - "}", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "d", - "s", - "t", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "a", - ")", - "-", - "[", - "r", - ":", - "{", - "r", - "e", - "l", - "}", - "]", - "-", - ">", - "(", - "b", - ")", - "\"", - " ", - "+", - " ", - "s", - "e", - "t", - "_", - "p", - "a", - "r", - "t", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "s", - "r", - "c", - "\"", - ":", - " ", - "s", - "r", - "c", - ",", - " ", - "\"", - "d", - "s", - "t", - "\"", - ":", - " ", - "d", - "s", - "t", - ",", - " ", - "*", - "*", - "p", - "r", - "o", - "p", - "s", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "r", - "e", - "a", - "t", - "e", - "d", - " ", - "{", - "l", - "e", - "n", - "(", - "e", - "d", - "g", - "e", - "s", - ")", - "}", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - "s", - "h", - "i", - "p", - "s", - "\"", - ")" + "print(f\"Created {len(edges)} relationships\")" ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 23689b2..735fdc1 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -55,6 +55,7 @@ " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", " _ctx = _ssl.create_default_context()\n", " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", @@ -108,1101 +109,39 @@ "metadata": {}, "outputs": [], "source": [ - "c", - "l", - "a", - "s", - "s", - " ", - "_", - "E", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "A", - "d", - "a", - "p", - "t", - "e", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "T", - "h", - "i", - "n", - " ", - "w", - "r", - "a", - "p", - "p", - "e", - "r", - " ", - "a", - "r", - "o", - "u", - "n", - "d", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - " ", - "t", - "h", - "a", - "t", - " ", - "a", - "d", - "d", - "s", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "-", - "c", - "o", - "m", - "p", - "a", - "t", - "i", - "b", - "l", - "e", - " ", - "m", - "e", - "t", - "h", - "o", - "d", - "s", - ".", - "\"", - "\"", - "\"", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "_", - "_", - "i", - "n", - "i", - "t", - "_", - "_", - "(", - "s", - "e", - "l", - "f", - ",", - " ", - "l", - "o", - "c", - "a", - "l", - "_", - "c", - "l", - "i", - "e", - "n", - "t", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - " ", - "=", - " ", - "l", - "o", - "c", - "a", - "l", - "_", - "c", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "s", - "e", - "l", - "f", - ",", - " ", - "q", - "u", - "e", - "r", - "y", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "N", - "o", - "n", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "q", - "u", - "e", - "r", - "y", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - " ", - "o", - "r", - " ", - "{", - "}", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "s", - "c", - "h", - "e", - "m", - "a", - "_", - "t", - "e", - "x", - "t", - "(", - "s", - "e", - "l", - "f", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "b", - "l", - "s", - " ", - "=", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - ")", - " ", - "U", - "N", - "W", - "I", - "N", - "D", - " ", - "l", - "a", - "b", - "e", - "l", - "s", - "(", - "n", - ")", - " ", - "A", - "S", - " ", - "l", - "b", - "l", - " ", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "D", - "I", - "S", - "T", - "I", - "N", - "C", - "T", - " ", - "l", - "b", - "l", - " ", - "O", - "R", - "D", - "E", - "R", - " ", - "B", - "Y", - " ", - "l", - "b", - "l", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "l", - "s", - " ", - "=", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - ")", - "-", - "[", - "r", - "]", - "-", - ">", - "(", - ")", - " ", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "D", - "I", - "S", - "T", - "I", - "N", - "C", - "T", - " ", - "t", - "y", - "p", - "e", - "(", - "r", - ")", - " ", - "A", - "S", - " ", - "t", - " ", - "O", - "R", - "D", - "E", - "R", - " ", - "B", - "Y", - " ", - "t", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - " ", - "=", - " ", - "[", - "\"", - "N", - "o", - "d", - "e", - " ", - "l", - "a", - "b", - "e", - "l", - "s", - ":", - "\"", - "]", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "l", - "b", - "l", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "f", - "\"", - " ", - " ", - "-", - " ", - "{", - "r", - "[", - "'", - "l", - "b", - "l", - "'", - "]", - "}", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "\"", - "\\", - "n", - "E", - "d", - "g", - "e", - " ", - "t", - "y", - "p", - "e", - "s", - ":", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "r", - "e", - "l", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "f", - "\"", - " ", - " ", - "-", - " ", - "{", - "r", - "[", - "'", - "t", - "'", - "]", - "}", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "\\", - "n", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "l", - "i", - "n", - "e", - "s", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "s", - "e", - "a", - "r", - "c", - "h", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - " ", - "—", - " ", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "r", - "u", - "n", - "n", - "i", - "n", - "g", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - ".", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "c", - "l", - "o", - "s", - "e", - "(", - "s", - "e", - "l", - "f", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "l", - "o", - "s", - "e", - "(", - ")", - "\n", + "class _EmbeddedAdapter:\n", + " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - "s", - "(", - "s", - "e", - "l", - "f", - ")", - ":", + " def __init__(self, local_client):\n", + " self._lc = local_client\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "S", - "c", - "h", - "e", - "m", - "a", - " ", - "i", - "n", - "t", - "r", - "o", - "s", - "p", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - ".", + " def cypher(self, query, params=None):\n", + " return self._lc.cypher(query, params or {})\n", "\n", + " def get_schema_text(self):\n", + " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", + " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", + " lines = [\"Node labels:\"]\n", + " for r in lbls:\n", + " lines.append(f\" - {r['lbl']}\")\n", + " lines.append(\"\\nEdge types:\")\n", + " for r in rels:\n", + " lines.append(f\" - {r['t']}\")\n", + " return \"\\n\".join(lines)\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "[", - "]", + " # Vector search not available in embedded mode — requires running CoordiNode server.\n", "\n", + " def close(self):\n", + " self._lc.close()\n", "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "e", - "d", - "g", - "e", - "_", - "t", - "y", - "p", - "e", - "s", - "(", - "s", - "e", - "l", - "f", - ")", - ":", + " def get_labels(self):\n", + " # Schema introspection not available in embedded mode.\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "S", - "c", - "h", - "e", - "m", - "a", - " ", - "i", - "n", - "t", - "r", - "o", - "s", - "p", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - ".", + " return []\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "[", - "]" + " def get_edge_types(self):\n", + " # Schema introspection not available in embedded mode.\n", + " return []" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index cf05d53..8c5ddf8 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -50,6 +50,7 @@ " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", " _ctx = _ssl.create_default_context()\n", " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", @@ -99,1101 +100,39 @@ "metadata": {}, "outputs": [], "source": [ - "c", - "l", - "a", - "s", - "s", - " ", - "_", - "E", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "A", - "d", - "a", - "p", - "t", - "e", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "T", - "h", - "i", - "n", - " ", - "w", - "r", - "a", - "p", - "p", - "e", - "r", - " ", - "a", - "r", - "o", - "u", - "n", - "d", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - " ", - "t", - "h", - "a", - "t", - " ", - "a", - "d", - "d", - "s", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "-", - "c", - "o", - "m", - "p", - "a", - "t", - "i", - "b", - "l", - "e", - " ", - "m", - "e", - "t", - "h", - "o", - "d", - "s", - ".", - "\"", - "\"", - "\"", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "_", - "_", - "i", - "n", - "i", - "t", - "_", - "_", - "(", - "s", - "e", - "l", - "f", - ",", - " ", - "l", - "o", - "c", - "a", - "l", - "_", - "c", - "l", - "i", - "e", - "n", - "t", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - " ", - "=", - " ", - "l", - "o", - "c", - "a", - "l", - "_", - "c", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "s", - "e", - "l", - "f", - ",", - " ", - "q", - "u", - "e", - "r", - "y", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "N", - "o", - "n", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "q", - "u", - "e", - "r", - "y", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - " ", - "o", - "r", - " ", - "{", - "}", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "s", - "c", - "h", - "e", - "m", - "a", - "_", - "t", - "e", - "x", - "t", - "(", - "s", - "e", - "l", - "f", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "b", - "l", - "s", - " ", - "=", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - ")", - " ", - "U", - "N", - "W", - "I", - "N", - "D", - " ", - "l", - "a", - "b", - "e", - "l", - "s", - "(", - "n", - ")", - " ", - "A", - "S", - " ", - "l", - "b", - "l", - " ", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "D", - "I", - "S", - "T", - "I", - "N", - "C", - "T", - " ", - "l", - "b", - "l", - " ", - "O", - "R", - "D", - "E", - "R", - " ", - "B", - "Y", - " ", - "l", - "b", - "l", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "l", - "s", - " ", - "=", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - ")", - "-", - "[", - "r", - "]", - "-", - ">", - "(", - ")", - " ", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "D", - "I", - "S", - "T", - "I", - "N", - "C", - "T", - " ", - "t", - "y", - "p", - "e", - "(", - "r", - ")", - " ", - "A", - "S", - " ", - "t", - " ", - "O", - "R", - "D", - "E", - "R", - " ", - "B", - "Y", - " ", - "t", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - " ", - "=", - " ", - "[", - "\"", - "N", - "o", - "d", - "e", - " ", - "l", - "a", - "b", - "e", - "l", - "s", - ":", - "\"", - "]", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "l", - "b", - "l", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "f", - "\"", - " ", - " ", - "-", - " ", - "{", - "r", - "[", - "'", - "l", - "b", - "l", - "'", - "]", - "}", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "\"", - "\\", - "n", - "E", - "d", - "g", - "e", - " ", - "t", - "y", - "p", - "e", - "s", - ":", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "r", - "e", - "l", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "l", - "i", - "n", - "e", - "s", - ".", - "a", - "p", - "p", - "e", - "n", - "d", - "(", - "f", - "\"", - " ", - " ", - "-", - " ", - "{", - "r", - "[", - "'", - "t", - "'", - "]", - "}", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "\\", - "n", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "l", - "i", - "n", - "e", - "s", - ")", - "\n", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "s", - "e", - "a", - "r", - "c", - "h", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - " ", - "—", - " ", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "r", - "u", - "n", - "n", - "i", - "n", - "g", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - ".", - "\n", - "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "c", - "l", - "o", - "s", - "e", - "(", - "s", - "e", - "l", - "f", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "e", - "l", - "f", - ".", - "_", - "l", - "c", - ".", - "c", - "l", - "o", - "s", - "e", - "(", - ")", - "\n", + "class _EmbeddedAdapter:\n", + " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - "s", - "(", - "s", - "e", - "l", - "f", - ")", - ":", + " def __init__(self, local_client):\n", + " self._lc = local_client\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "S", - "c", - "h", - "e", - "m", - "a", - " ", - "i", - "n", - "t", - "r", - "o", - "s", - "p", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - ".", + " def cypher(self, query, params=None):\n", + " return self._lc.cypher(query, params or {})\n", "\n", + " def get_schema_text(self):\n", + " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", + " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", + " lines = [\"Node labels:\"]\n", + " for r in lbls:\n", + " lines.append(f\" - {r['lbl']}\")\n", + " lines.append(\"\\nEdge types:\")\n", + " for r in rels:\n", + " lines.append(f\" - {r['t']}\")\n", + " return \"\\n\".join(lines)\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "[", - "]", + " # Vector search not available in embedded mode — requires running CoordiNode server.\n", "\n", + " def close(self):\n", + " self._lc.close()\n", "\n", - " ", - " ", - " ", - " ", - "d", - "e", - "f", - " ", - "g", - "e", - "t", - "_", - "e", - "d", - "g", - "e", - "_", - "t", - "y", - "p", - "e", - "s", - "(", - "s", - "e", - "l", - "f", - ")", - ":", + " def get_labels(self):\n", + " # Schema introspection not available in embedded mode.\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "#", - " ", - "S", - "c", - "h", - "e", - "m", - "a", - " ", - "i", - "n", - "t", - "r", - "o", - "s", - "p", - "e", - "c", - "t", - "i", - "o", - "n", - " ", - "n", - "o", - "t", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "i", - "n", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - ".", + " return []\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "[", - "]" + " def get_edge_types(self):\n", + " # Schema introspection not available in embedded mode.\n", + " return []" ] }, { diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 8d1b3cf..a6cb8e1 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -51,6 +51,7 @@ " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", " _ctx = _ssl.create_default_context()\n", " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", @@ -201,9 +202,7 @@ " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", " if not _SESSION_SCOPE_RE.search(q):\n", - " return (\n", - " \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", - " )\n", + " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", " rows = client.cypher(q, params={\"sess\": SESSION})\n", " return str(rows[:20]) if rows else \"No results\"\n", "\n", From 1a6d66b9f0d2a8c1fe9763f9648c0bb6257b4c99 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 10:11:07 +0300 Subject: [PATCH 26/86] fix(notebooks,types): session scoping, conditional embedded install, demo_tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coordinode/_types.py: use tuple form in isinstance() (list|tuple → (list,tuple), int|float → (int,float)) for consistent style across the SDK - notebooks 00-03: move coordinode-embedded VCS install into if IN_COLAB: block; add ~/.cargo/bin to PATH after rustup so maturin/cargo are findable - 00_seed_data: add DEMO_TAG variable; scope all node MERGEs and cleanup DETACH DELETE with demo_tag predicate to avoid tagging pre-existing server nodes - 02_langchain: add "langchain" to pip install list; add allow_dangerous_requests=True to GraphCypherQAChain.from_llm() for current LangChain API compatibility - 03_langgraph: scope destination node m to session in find_related() Cypher query (MATCH (m:Entity {session: $sess})) to prevent cross-session data leakage --- coordinode/coordinode/_types.py | 4 +- demo/notebooks/00_seed_data.ipynb | 8328 ++++++++++++++++- .../01_llama_index_property_graph.ipynb | 2003 +++- demo/notebooks/02_langchain_graph_chain.ipynb | 2543 ++++- demo/notebooks/03_langgraph_agent.ipynb | 5718 ++++++++++- 5 files changed, 18103 insertions(+), 493 deletions(-) diff --git a/coordinode/coordinode/_types.py b/coordinode/coordinode/_types.py index dbfa035..ef45edd 100644 --- a/coordinode/coordinode/_types.py +++ b/coordinode/coordinode/_types.py @@ -33,11 +33,11 @@ def to_property_value(py_val: PyValue) -> Any: pv.string_value = py_val elif isinstance(py_val, bytes): pv.bytes_value = py_val - elif isinstance(py_val, list | tuple): + elif isinstance(py_val, (list, tuple)): # Homogeneous float list → Vector; mixed/str list → PropertyList. # bool is a subclass of int, so exclude it explicitly — [True, False] must # not be serialised as a Vector of 1.0/0.0 but as a PropertyList. - if py_val and all(isinstance(v, int | float) and not isinstance(v, bool) for v in py_val): + if py_val and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in py_val): vec = Vector(values=[float(v) for v in py_val]) pv.vector_value.CopyFrom(vec) else: diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 88c6d6d..2d37acc 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -1146,280 +1146,8082 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"Ready\")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000004", - "metadata": {}, - "source": [ - "## Connect to CoordiNode\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "y", + "s", + ",", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", "\n", - "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", - "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", - "- **Local without server**: falls back to embedded engine automatically." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000005", - "metadata": {}, - "outputs": [], - "source": [ - "import os, socket\n", "\n", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + " ", + "=", + " ", + "\"", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "l", + "a", + "b", + "\"", + " ", + "i", + "n", + " ", + "s", + "y", + "s", + ".", + "m", + "o", + "d", + "u", + "l", + "e", + "s", "\n", - "def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", "\n", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "o", + "n", + "l", + "y", + " ", + "w", + "h", + "e", + "n", + " ", + "n", + "o", + " ", + "g", + "R", + "P", + "C", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "i", + "s", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "(", + "C", + "o", + "l", + "a", + "b", + " ", + "p", + "a", + "t", + "h", + ")", + ".", "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "i", + "f", + " ", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + ":", "\n", - "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " from coordinode import CoordinodeClient\n", + " ", + " ", + " ", + " ", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "R", + "u", + "s", + "t", + " ", + "t", + "o", + "o", + "l", + "c", + "h", + "a", + "i", + "n", + " ", + "v", + "i", + "a", + " ", + "r", + "u", + "s", + "t", + "u", + "p", + " ", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + ")", + ".", "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", + " ", + " ", + " ", + " ", + "#", + " ", + "C", + "o", + "l", + "a", + "b", + "'", + "s", + " ", + "a", + "p", + "t", + " ", + "p", + "a", + "c", + "k", + "a", + "g", + "e", + "s", + " ", + "s", + "h", + "i", + "p", + " ", + "r", + "u", + "s", + "t", + "c", + " ", + "≤", + "1", + ".", + "7", + "5", + ",", + " ", + "w", + "h", + "i", + "c", + "h", + " ", + "c", + "a", + "n", + "n", + "o", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "≥", + "1", + ".", + "8", + "0", + " ", + "f", + "o", + "r", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "y", + "o", + "3", + ")", + ".", + " ", + "a", + "p", + "t", + "-", + "g", + "e", + "t", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "a", + " ", + "v", + "i", + "a", + "b", + "l", + "e", + " ", + "a", + "l", + "t", + "e", + "r", + "n", + "a", + "t", + "i", + "v", + "e", + " ", + "h", + "e", + "r", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "D", + "o", + "w", + "n", + "l", + "o", + "a", + "d", + " ", + "t", + "h", + "e", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "r", + " ", + "t", + "o", + " ", + "a", + " ", + "t", + "e", + "m", + "p", + " ", + "f", + "i", + "l", + "e", + " ", + "a", + "n", + "d", + " ", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + " ", + "i", + "t", + " ", + "e", + "x", + "p", + "l", + "i", + "c", + "i", + "t", + "l", + "y", + " ", + "—", + " ", + "t", + "h", + "i", + "s", + " ", + "a", + "v", + "o", + "i", + "d", + "s", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "p", + "i", + "p", + "i", + "n", + "g", + " ", + "r", + "e", + "m", + "o", + "t", + "e", + " ", + "c", + "o", + "n", + "t", + "e", + "n", + "t", + " ", + "d", + "i", + "r", + "e", + "c", + "t", + "l", + "y", + " ", + "i", + "n", + "t", + "o", + " ", + "a", + " ", + "s", + "h", + "e", + "l", + "l", + " ", + "w", + "h", + "i", + "l", + "e", + " ", + "m", + "a", + "i", + "n", + "t", + "a", + "i", + "n", + "i", + "n", + "g", + " ", + "H", + "T", + "T", + "P", + "S", + "/", + "T", + "L", + "S", + " ", + "s", + "e", + "c", + "u", + "r", + "i", + "t", + "y", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "t", + "h", + "r", + "o", + "u", + "g", + "h", + " ", + "P", + "y", + "t", + "h", + "o", + "n", + "'", + "s", + " ", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + " ", + "s", + "s", + "l", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + " ", + "(", + "c", + "e", + "r", + "t", + "-", + "v", + "e", + "r", + "i", + "f", + "i", + "e", + "d", + ",", + " ", + "T", + "L", + "S", + " ", + "1", + ".", + "2", + "+", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "s", + "s", + "l", + " ", + "a", + "s", + " ", + "_", + "s", + "s", + "l", + ",", + " ", + "t", + "e", + "m", + "p", + "f", + "i", + "l", + "e", + " ", + "a", + "s", + " ", + "_", + "t", + "m", + "p", + ",", + " ", + "u", + "r", + "l", + "l", + "i", + "b", + ".", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + " ", + "a", + "s", + " ", + "_", + "u", + "r", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "t", + "x", + " ", + "=", + " ", + "_", + "s", + "s", + "l", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + "_", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "t", + "m", + "p", + ".", + "N", + "a", + "m", + "e", + "d", + "T", + "e", + "m", + "p", + "o", + "r", + "a", + "r", + "y", + "F", + "i", + "l", + "e", + "(", + "m", + "o", + "d", + "e", + "=", + "\"", + "w", + "b", + "\"", + ",", + " ", + "s", + "u", + "f", + "f", + "i", + "x", + "=", + "\"", + ".", + "s", + "h", + "\"", + ",", + " ", + "d", + "e", + "l", + "e", + "t", + "e", + "=", + "F", + "a", + "l", + "s", + "e", + ")", + " ", + "a", + "s", + " ", + "_", + "f", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "u", + "r", + ".", + "u", + "r", + "l", + "o", + "p", + "e", + "n", + "(", + "\"", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "s", + "h", + ".", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + "\"", + ",", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "=", + "_", + "c", + "t", + "x", + ")", + " ", + "a", + "s", + " ", + "_", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "f", + ".", + "w", + "r", + "i", + "t", + "e", + "(", + "_", + "r", + ".", + "r", + "e", + "a", + "d", + "(", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + " ", + "=", + " ", + "_", + "f", + ".", + "n", + "a", + "m", + "e", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "\"", + "/", + "b", + "i", + "n", + "/", + "s", + "h", + "\"", + ",", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ",", + " ", + "\"", + "-", + "s", + "\"", + ",", + " ", + "\"", + "-", + "-", + "\"", + ",", + " ", + "\"", + "-", + "y", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "f", + "i", + "n", + "a", + "l", + "l", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "u", + "n", + "l", + "i", + "n", + "k", + "(", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ")", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "d", + "d", + " ", + "c", + "a", + "r", + "g", + "o", + " ", + "t", + "o", + " ", + "P", + "A", + "T", + "H", + " ", + "s", + "o", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "i", + "p", + " ", + "c", + "a", + "n", + " ", + "f", + "i", + "n", + "d", + " ", + "i", + "t", + ".", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + " ", + "=", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "p", + "a", + "n", + "d", + "u", + "s", + "e", + "r", + "(", + "\"", + "~", + "/", + ".", + "c", + "a", + "r", + "g", + "o", + "/", + "b", + "i", + "n", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "P", + "A", + "T", + "H", + "\"", + "]", + " ", + "=", + " ", + "f", + "\"", + "{", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + "}", + "{", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + "s", + "e", + "p", + "}", + "{", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "'", + "P", + "A", + "T", + "H", + "'", + ",", + " ", + "'", + "'", + ")", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + " ", + "\"", + "-", + "m", + "\"", + ",", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + ",", + " ", + "\"", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + ")", + "\n", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + "\n", + "\n", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + ".", + "a", + "p", + "p", + "l", + "y", + "(", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "R", + "e", + "a", + "d", + "y", + "\"", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Connect to CoordiNode\n", + "\n", + "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", + "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", + "- **Local without server**: falls back to embedded engine automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": [ + "import os, socket\n", + "\n", + "\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " from coordinode import CoordinodeClient\n", "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "else:\n", - " # No server available — use the embedded in-process engine.\n", - " from coordinode_embedded import LocalClient\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " from coordinode_embedded import LocalClient\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## Step 1 — Clear previous demo data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + " ", + "=", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + "\"", + ",", + " ", + "\"", + "s", + "e", + "e", + "d", + "_", + "d", + "a", + "t", + "a", + "\"", + ")", + "\n", + "r", + "e", + "s", + "u", + "l", + "t", + " ", + "=", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + " ", + "{", + "d", + "e", + "m", + "o", + ":", + " ", + "t", + "r", + "u", + "e", + ",", + " ", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + ":", + " ", + "$", + "t", + "a", + "g", + "}", + ")", + " ", + "D", + "E", + "T", + "A", + "C", + "H", + " ", + "D", + "E", + "L", + "E", + "T", + "E", + " ", + "n", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "t", + "a", + "g", + "\"", + ":", + " ", + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + "}", + ",", + "\n", + ")", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "P", + "r", + "e", + "v", + "i", + "o", + "u", + "s", + " ", + "d", + "e", + "m", + "o", + " ", + "d", + "a", + "t", + "a", + " ", + "r", + "e", + "m", + "o", + "v", + "e", + "d", + "\"", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## Step 2 — Create nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "#", + " ", + "─", + "─", + " ", + "P", + "e", + "o", + "p", + "l", + "e", + " ", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "\n", + "p", + "e", + "o", + "p", + "l", + "e", + " ", + "=", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "M", + "L", + " ", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "e", + "r", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "R", + "e", + "i", + "n", + "f", + "o", + "r", + "c", + "e", + "m", + "e", + "n", + "t", + " ", + "L", + "e", + "a", + "r", + "n", + "i", + "n", + "g", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "B", + "o", + "b", + " ", + "T", + "o", + "r", + "r", + "e", + "s", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "S", + "t", + "a", + "f", + "f", + " ", + "E", + "n", + "g", + "i", + "n", + "e", + "e", + "r", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "D", + "i", + "s", + "t", + "r", + "i", + "b", + "u", + "t", + "e", + "d", + " ", + "S", + "y", + "s", + "t", + "e", + "m", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "F", + "o", + "u", + "n", + "d", + "e", + "r", + " ", + "&", + " ", + "C", + "E", + "O", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "N", + "L", + "P", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + " ", + "S", + "c", + "i", + "e", + "n", + "t", + "i", + "s", + "t", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "L", + "L", + "M", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "E", + "v", + "a", + " ", + "M", + "ü", + "l", + "l", + "e", + "r", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "S", + "y", + "s", + "t", + "e", + "m", + "s", + " ", + "A", + "r", + "c", + "h", + "i", + "t", + "e", + "c", + "t", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "P", + "r", + "i", + "n", + "c", + "i", + "p", + "a", + "l", + " ", + "E", + "n", + "g", + "i", + "n", + "e", + "e", + "r", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "M", + "e", + "t", + "a", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "M", + "L", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "P", + "h", + "D", + " ", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "e", + "r", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "M", + "I", + "T", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "H", + "e", + "n", + "r", + "y", + " ", + "R", + "o", + "s", + "s", + "i", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "C", + "T", + "O", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "S", + "e", + "n", + "i", + "o", + "r", + " ", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "e", + "r", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "s", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "J", + "a", + "m", + "e", + "s", + " ", + "W", + "r", + "i", + "g", + "h", + "t", + "\"", + ",", + " ", + "\"", + "r", + "o", + "l", + "e", + "\"", + ":", + " ", + "\"", + "E", + "n", + "g", + "i", + "n", + "e", + "e", + "r", + "i", + "n", + "g", + " ", + "L", + "e", + "a", + "d", + "\"", + ",", + " ", + "\"", + "o", + "r", + "g", + "\"", + ":", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "f", + "i", + "e", + "l", + "d", + "\"", + ":", + " ", + "\"", + "S", + "e", + "a", + "r", + "c", + "h", + "\"", + "}", + ",", + "\n", + "]", + "\n", + "\n", + "f", + "o", + "r", + " ", + "p", + " ", + "i", + "n", + " ", + "p", + "e", + "o", + "p", + "l", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "n", + ":", + "P", + "e", + "r", + "s", + "o", + "n", + " ", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "n", + "a", + "m", + "e", + "}", + ")", + " ", + "S", + "E", + "T", + " ", + "n", + ".", + "r", + "o", + "l", + "e", + " ", + "=", + " ", + "$", + "r", + "o", + "l", + "e", + ",", + " ", + "n", + ".", + "f", + "i", + "e", + "l", + "d", + " ", + "=", + " ", + "$", + "f", + "i", + "e", + "l", + "d", + ",", + " ", + "n", + ".", + "d", + "e", + "m", + "o", + " ", + "=", + " ", + "t", + "r", + "u", + "e", + "\"", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "p", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "r", + "e", + "a", + "t", + "e", + "d", + " ", + "{", + "l", + "e", + "n", + "(", + "p", + "e", + "o", + "p", + "l", + "e", + ")", + "}", + " ", + "p", + "e", + "o", + "p", + "l", + "e", + "\"", + ")", + "\n", + "\n", + "#", + " ", + "─", + "─", + " ", + "C", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + " ", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "\n", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + " ", + "=", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "y", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "1", + "9", + "9", + "8", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "M", + "o", + "u", + "n", + "t", + "a", + "i", + "n", + " ", + "V", + "i", + "e", + "w", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "M", + "e", + "t", + "a", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "y", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "2", + "0", + "0", + "4", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "M", + "e", + "n", + "l", + "o", + " ", + "P", + "a", + "r", + "k", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "A", + "I", + " ", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "2", + "0", + "1", + "5", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "S", + "a", + "n", + " ", + "F", + "r", + "a", + "n", + "c", + "i", + "s", + "c", + "o", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "A", + "I", + " ", + "R", + "e", + "s", + "e", + "a", + "r", + "c", + "h", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "2", + "0", + "1", + "0", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "L", + "o", + "n", + "d", + "o", + "n", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "A", + "I", + " ", + "S", + "t", + "a", + "r", + "t", + "u", + "p", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "B", + "e", + "r", + "l", + "i", + "n", + "\"", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "M", + "I", + "T", + "\"", + ",", + " ", + "\"", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + "\"", + ":", + " ", + "\"", + "A", + "c", + "a", + "d", + "e", + "m", + "i", + "a", + "\"", + ",", + " ", + "\"", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + "\"", + ":", + " ", + "1", + "8", + "6", + "1", + ",", + " ", + "\"", + "h", + "q", + "\"", + ":", + " ", + "\"", + "C", + "a", + "m", + "b", + "r", + "i", + "d", + "g", + "e", + "\"", + "}", + ",", + "\n", + "]", + "\n", + "\n", + "f", + "o", + "r", + " ", + "c", + " ", + "i", + "n", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "n", + ":", + "C", + "o", + "m", + "p", + "a", + "n", + "y", + " ", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "n", + "a", + "m", + "e", + ",", + " ", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + ":", + " ", + "$", + "t", + "a", + "g", + "}", + ")", + " ", + "S", + "E", + "T", + " ", + "n", + ".", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + " ", + "=", + " ", + "$", + "i", + "n", + "d", + "u", + "s", + "t", + "r", + "y", + ",", + " ", + "n", + ".", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + " ", + "=", + " ", + "$", + "f", + "o", + "u", + "n", + "d", + "e", + "d", + ",", + " ", + "n", + ".", + "h", + "q", + " ", + "=", + " ", + "$", + "h", + "q", + ",", + " ", + "n", + ".", + "d", + "e", + "m", + "o", + " ", + "=", + " ", + "t", + "r", + "u", + "e", + ",", + " ", + "n", + ".", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + " ", + "=", + " ", + "$", + "t", + "a", + "g", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "*", + "*", + "c", + ",", + " ", + "\"", + "t", + "a", + "g", + "\"", + ":", + " ", + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "r", + "e", + "a", + "t", + "e", + "d", + " ", + "{", + "l", + "e", + "n", + "(", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + ")", + "}", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + "\"", + ")", + "\n", + "\n", + "#", + " ", + "─", + "─", + " ", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + " ", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "─", + "\n", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + " ", + "=", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "A", + "r", + "c", + "h", + "i", + "t", + "e", + "c", + "t", + "u", + "r", + "e", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "7", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "A", + "l", + "g", + "o", + "r", + "i", + "t", + "h", + "m", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "0", + "9", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "R", + "e", + "i", + "n", + "f", + "o", + "r", + "c", + "e", + "m", + "e", + "n", + "t", + " ", + "L", + "e", + "a", + "r", + "n", + "i", + "n", + "g", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "P", + "a", + "r", + "a", + "d", + "i", + "g", + "m", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "1", + "9", + "8", + "0", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "D", + "a", + "t", + "a", + " ", + "M", + "o", + "d", + "e", + "l", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "2", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "I", + "n", + "f", + "r", + "a", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "9", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "i", + "q", + "u", + "e", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "0", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "L", + "L", + "M", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "M", + "o", + "d", + "e", + "l", + " ", + "C", + "l", + "a", + "s", + "s", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "8", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "t", + "y", + "p", + "e", + "\"", + ":", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "i", + "q", + "u", + "e", + "\"", + ",", + " ", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "3", + "}", + ",", + "\n", + "]", + "\n", + "\n", + "f", + "o", + "r", + " ", + "t", + " ", + "i", + "n", + " ", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "n", + ":", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "y", + " ", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "n", + "a", + "m", + "e", + ",", + " ", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + ":", + " ", + "$", + "t", + "a", + "g", + "}", + ")", + " ", + "S", + "E", + "T", + " ", + "n", + ".", + "t", + "y", + "p", + "e", + " ", + "=", + " ", + "$", + "t", + "y", + "p", + "e", + ",", + " ", + "n", + ".", + "y", + "e", + "a", + "r", + " ", + "=", + " ", + "$", + "y", + "e", + "a", + "r", + ",", + " ", + "n", + ".", + "d", + "e", + "m", + "o", + " ", + "=", + " ", + "t", + "r", + "u", + "e", + ",", + " ", + "n", + ".", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + " ", + "=", + " ", + "$", + "t", + "a", + "g", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "*", + "*", + "t", + ",", + " ", + "\"", + "t", + "a", + "g", + "\"", + ":", + " ", + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "r", + "e", + "a", + "t", + "e", + "d", + " ", + "{", + "l", + "e", + "n", + "(", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + ")", + "}", + " ", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + "\"", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## Step 3 — Create relationships" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "e", + "d", + "g", + "e", + "s", + " ", + "=", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "B", + "o", + "b", + " ", + "T", + "o", + "r", + "r", + "e", + "s", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "E", + "v", + "a", + " ", + "M", + "ü", + "l", + "l", + "e", + "r", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "2", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "M", + "e", + "t", + "a", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "M", + "I", + "T", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "H", + "e", + "n", + "r", + "y", + " ", + "R", + "o", + "s", + "s", + "i", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "J", + "a", + "m", + "e", + "s", + " ", + "W", + "r", + "i", + "g", + "h", + "t", + "\"", + ",", + " ", + "\"", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "\"", + ",", + " ", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "H", + "e", + "n", + "r", + "y", + " ", + "R", + "o", + "s", + "s", + "i", + "\"", + ",", + " ", + "\"", + "C", + "O", + "_", + "F", + "O", + "U", + "N", + "D", + "E", + "D", + "\"", + ",", + " ", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "K", + "N", + "O", + "W", + "S", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "C", + "a", + "r", + "o", + "l", + " ", + "S", + "m", + "i", + "t", + "h", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "B", + "o", + "b", + " ", + "T", + "o", + "r", + "r", + "e", + "s", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "J", + "a", + "m", + "e", + "s", + " ", + "W", + "r", + "i", + "g", + "h", + "t", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "E", + "v", + "a", + " ", + "M", + "ü", + "l", + "l", + "e", + "r", + "\"", + ",", + " ", + "\"", + "K", + "N", + "O", + "W", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + " ", + "/", + " ", + "W", + "O", + "R", + "K", + "S", + "_", + "O", + "N", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "A", + "l", + "i", + "c", + "e", + " ", + "C", + "h", + "e", + "n", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "R", + "e", + "i", + "n", + "f", + "o", + "r", + "c", + "e", + "m", + "e", + "n", + "t", + " ", + "L", + "e", + "a", + "r", + "n", + "i", + "n", + "g", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "1", + "9", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "D", + "a", + "v", + "i", + "d", + " ", + "P", + "a", + "r", + "k", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "L", + "L", + "M", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "0", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "1", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "I", + "s", + "l", + "a", + " ", + "N", + "a", + "k", + "a", + "m", + "u", + "r", + "a", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "0", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "F", + "r", + "a", + "n", + "k", + " ", + "L", + "i", + "u", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + " ", + "N", + "e", + "u", + "r", + "a", + "l", + " ", + "N", + "e", + "t", + "w", + "o", + "r", + "k", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "c", + "e", + " ", + "O", + "k", + "a", + "f", + "o", + "r", + "\"", + ",", + " ", + "\"", + "R", + "E", + "S", + "E", + "A", + "R", + "C", + "H", + "E", + "S", + "\"", + ",", + " ", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "\"", + "s", + "i", + "n", + "c", + "e", + "\"", + ":", + " ", + "2", + "0", + "2", + "3", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "U", + "S", + "E", + "S", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "S", + "y", + "n", + "t", + "h", + "e", + "x", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "O", + "p", + "e", + "n", + "A", + "I", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "U", + "S", + "E", + "S", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "C", + "Q", + "U", + "I", + "R", + "E", + "D", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "o", + "o", + "g", + "l", + "e", + "\"", + ",", + " ", + "\"", + "A", + "C", + "Q", + "U", + "I", + "R", + "E", + "D", + "\"", + ",", + " ", + "\"", + "D", + "e", + "e", + "p", + "M", + "i", + "n", + "d", + "\"", + ",", + " ", + "{", + "\"", + "y", + "e", + "a", + "r", + "\"", + ":", + " ", + "2", + "0", + "1", + "4", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "K", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "G", + "r", + "a", + "p", + "h", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "G", + "r", + "a", + "p", + "h", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "R", + "A", + "G", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "V", + "e", + "c", + "t", + "o", + "r", + " ", + "D", + "a", + "t", + "a", + "b", + "a", + "s", + "e", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + "(", + "\"", + "L", + "L", + "M", + "\"", + ",", + " ", + "\"", + "B", + "U", + "I", + "L", + "D", + "S", + "_", + "O", + "N", + "\"", + ",", + " ", + "\"", + "T", + "r", + "a", + "n", + "s", + "f", + "o", + "r", + "m", + "e", + "r", + "\"", + ",", + " ", + "{", + "}", + ")", + ",", + "\n", + "]", + "\n", + "\n", + "s", + "r", + "c", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "p", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "p", + " ", + "i", + "n", + " ", + "p", + "e", + "o", + "p", + "l", + "e", + "}", + "\n", + "t", + "e", + "c", + "h", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "t", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "t", + " ", + "i", + "n", + " ", + "t", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "i", + "e", + "s", + "}", + "\n", + "c", + "o", + "m", + "p", + "a", + "n", + "y", + "_", + "n", + "a", + "m", + "e", + "s", + " ", + "=", + " ", + "{", + "c", + "[", + "\"", + "n", + "a", + "m", + "e", + "\"", + "]", + " ", + "f", + "o", + "r", + " ", + "c", + " ", + "i", + "n", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "i", + "e", + "s", + "}", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "n", + "a", + "m", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "s", + "r", + "c", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "P", + "e", + "r", + "s", + "o", + "n", + "\"", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "t", + "e", + "c", + "h", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "T", + "e", + "c", + "h", + "n", + "o", + "l", + "o", + "g", + "y", + "\"", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "a", + "m", + "e", + " ", + "i", + "n", + " ", + "c", + "o", + "m", + "p", + "a", + "n", + "y", + "_", + "n", + "a", + "m", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "C", + "o", + "m", + "p", + "a", + "n", + "y", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "a", + "i", + "s", + "e", + " ", + "V", + "a", + "l", + "u", + "e", + "E", + "r", + "r", + "o", + "r", + "(", + "f", + "\"", + "U", + "n", + "k", + "n", + "o", + "w", + "n", + " ", + "e", + "d", + "g", + "e", + " ", + "e", + "n", + "d", + "p", + "o", + "i", + "n", + "t", + ":", + " ", + "{", + "n", + "a", + "m", + "e", + "!", + "r", + "}", + "\"", + ")", + "\n", + "\n", + "\n", + "f", + "o", + "r", + " ", + "s", + "r", + "c", + ",", + " ", + "r", + "e", + "l", + ",", + " ", + "d", + "s", + "t", + ",", + " ", + "p", + "r", + "o", + "p", + "s", + " ", + "i", + "n", + " ", + "e", + "d", + "g", + "e", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + "s", + "r", + "c", + "_", + "l", + "a", + "b", + "e", + "l", + " ", + "=", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "s", + "r", + "c", + ")", + "\n", + " ", + " ", + " ", + " ", + "d", + "s", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + " ", + "=", + " ", + "_", + "l", + "a", + "b", + "e", + "l", + "(", + "d", + "s", + "t", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + " ", + "=", + " ", + "\"", + ",", + " ", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "f", + "\"", + "r", + ".", + "{", + "k", + "}", + " ", + "=", + " ", + "$", + "{", + "k", + "}", + "\"", + " ", + "f", + "o", + "r", + " ", + "k", + " ", + "i", + "n", + " ", + "p", + "r", + "o", + "p", + "s", + ")", + " ", + "i", + "f", + " ", + "p", + "r", + "o", + "p", + "s", + " ", + "e", + "l", + "s", + "e", + " ", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "e", + "t", + "_", + "p", + "a", + "r", + "t", + " ", + "=", + " ", + "f", + "\"", + " ", + "S", + "E", + "T", + " ", + "{", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + "}", + "\"", + " ", + "i", + "f", + " ", + "s", + "e", + "t", + "_", + "c", + "l", + "a", + "u", + "s", + "e", + " ", + "e", + "l", + "s", + "e", + " ", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "a", + ":", + "{", + "s", + "r", + "c", + "_", + "l", + "a", + "b", + "e", + "l", + "}", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "s", + "r", + "c", + ",", + " ", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + ":", + " ", + "$", + "t", + "a", + "g", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "b", + ":", + "{", + "d", + "s", + "t", + "_", + "l", + "a", + "b", + "e", + "l", + "}", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "d", + "s", + "t", + ",", + " ", + "d", + "e", + "m", + "o", + "_", + "t", + "a", + "g", + ":", + " ", + "$", + "t", + "a", + "g", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "a", + ")", + "-", + "[", + "r", + ":", + "{", + "r", + "e", + "l", + "}", + "]", + "-", + ">", + "(", + "b", + ")", + "\"", + " ", + "+", + " ", + "s", + "e", + "t", + "_", + "p", + "a", + "r", + "t", + ",", "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000006", - "metadata": {}, - "source": [ - "## Step 1 — Clear previous demo data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000007", - "metadata": {}, - "outputs": [], - "source": [ - "result = client.cypher(\"MATCH (n {demo: true}) DETACH DELETE n\")\n", - "print(\"Previous demo data removed\")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000008", - "metadata": {}, - "source": [ - "## Step 2 — Create nodes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000009", - "metadata": {}, - "outputs": [], - "source": [ - "# ── People ────────────────────────────────────────────────────────────────\n", - "people = [\n", - " {\"name\": \"Alice Chen\", \"role\": \"ML Researcher\", \"org\": \"DeepMind\", \"field\": \"Reinforcement Learning\"},\n", - " {\"name\": \"Bob Torres\", \"role\": \"Staff Engineer\", \"org\": \"Google\", \"field\": \"Distributed Systems\"},\n", - " {\"name\": \"Carol Smith\", \"role\": \"Founder & CEO\", \"org\": \"Synthex\", \"field\": \"NLP\"},\n", - " {\"name\": \"David Park\", \"role\": \"Research Scientist\", \"org\": \"OpenAI\", \"field\": \"LLMs\"},\n", - " {\"name\": \"Eva Müller\", \"role\": \"Systems Architect\", \"org\": \"Synthex\", \"field\": \"Graph Databases\"},\n", - " {\"name\": \"Frank Liu\", \"role\": \"Principal Engineer\", \"org\": \"Meta\", \"field\": \"Graph ML\"},\n", - " {\"name\": \"Grace Okafor\", \"role\": \"PhD Researcher\", \"org\": \"MIT\", \"field\": \"Knowledge Graphs\"},\n", - " {\"name\": \"Henry Rossi\", \"role\": \"CTO\", \"org\": \"Synthex\", \"field\": \"Databases\"},\n", - " {\"name\": \"Isla Nakamura\", \"role\": \"Senior Researcher\", \"org\": \"DeepMind\", \"field\": \"Graph Neural Networks\"},\n", - " {\"name\": \"James Wright\", \"role\": \"Engineering Lead\", \"org\": \"Google\", \"field\": \"Search\"},\n", - "]\n", - "\n", - "for p in people:\n", - " client.cypher(\"MERGE (n:Person {name: $name}) SET n.role = $role, n.field = $field, n.demo = true\", params=p)\n", - "\n", - "print(f\"Created {len(people)} people\")\n", - "\n", - "# ── Companies ─────────────────────────────────────────────────────────────\n", - "companies = [\n", - " {\"name\": \"Google\", \"industry\": \"Technology\", \"founded\": 1998, \"hq\": \"Mountain View\"},\n", - " {\"name\": \"Meta\", \"industry\": \"Technology\", \"founded\": 2004, \"hq\": \"Menlo Park\"},\n", - " {\"name\": \"OpenAI\", \"industry\": \"AI Research\", \"founded\": 2015, \"hq\": \"San Francisco\"},\n", - " {\"name\": \"DeepMind\", \"industry\": \"AI Research\", \"founded\": 2010, \"hq\": \"London\"},\n", - " {\"name\": \"Synthex\", \"industry\": \"AI Startup\", \"founded\": 2021, \"hq\": \"Berlin\"},\n", - " {\"name\": \"MIT\", \"industry\": \"Academia\", \"founded\": 1861, \"hq\": \"Cambridge\"},\n", - "]\n", - "\n", - "for c in companies:\n", - " client.cypher(\n", - " \"MERGE (n:Company {name: $name}) SET n.industry = $industry, n.founded = $founded, n.hq = $hq, n.demo = true\",\n", - " params=c,\n", - " )\n", - "\n", - "print(f\"Created {len(companies)} companies\")\n", - "\n", - "# ── Technologies ──────────────────────────────────────────────────────────\n", - "technologies = [\n", - " {\"name\": \"Transformer\", \"type\": \"Architecture\", \"year\": 2017},\n", - " {\"name\": \"Graph Neural Network\", \"type\": \"Algorithm\", \"year\": 2009},\n", - " {\"name\": \"Reinforcement Learning\", \"type\": \"Paradigm\", \"year\": 1980},\n", - " {\"name\": \"Knowledge Graph\", \"type\": \"Data Model\", \"year\": 2012},\n", - " {\"name\": \"Vector Database\", \"type\": \"Infrastructure\", \"year\": 2019},\n", - " {\"name\": \"RAG\", \"type\": \"Technique\", \"year\": 2020},\n", - " {\"name\": \"LLM\", \"type\": \"Model Class\", \"year\": 2018},\n", - " {\"name\": \"GraphRAG\", \"type\": \"Technique\", \"year\": 2023},\n", - "]\n", - "\n", - "for t in technologies:\n", - " client.cypher(\"MERGE (n:Technology {name: $name}) SET n.type = $type, n.year = $year, n.demo = true\", params=t)\n", - "\n", - "print(f\"Created {len(technologies)} technologies\")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000010", - "metadata": {}, - "source": [ - "## Step 3 — Create relationships" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000011", - "metadata": {}, - "outputs": [], - "source": [ - "edges = [\n", - " # WORKS_AT\n", - " (\"Alice Chen\", \"WORKS_AT\", \"DeepMind\", {}),\n", - " (\"Bob Torres\", \"WORKS_AT\", \"Google\", {}),\n", - " (\"Carol Smith\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", - " (\"David Park\", \"WORKS_AT\", \"OpenAI\", {}),\n", - " (\"Eva Müller\", \"WORKS_AT\", \"Synthex\", {\"since\": 2022}),\n", - " (\"Frank Liu\", \"WORKS_AT\", \"Meta\", {}),\n", - " (\"Grace Okafor\", \"WORKS_AT\", \"MIT\", {}),\n", - " (\"Henry Rossi\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", - " (\"Isla Nakamura\", \"WORKS_AT\", \"DeepMind\", {}),\n", - " (\"James Wright\", \"WORKS_AT\", \"Google\", {}),\n", - " # FOUNDED\n", - " (\"Carol Smith\", \"FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", - " (\"Henry Rossi\", \"CO_FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", - " # KNOWS\n", - " (\"Alice Chen\", \"KNOWS\", \"Isla Nakamura\", {}),\n", - " (\"Alice Chen\", \"KNOWS\", \"David Park\", {}),\n", - " (\"Carol Smith\", \"KNOWS\", \"Bob Torres\", {}),\n", - " (\"Grace Okafor\", \"KNOWS\", \"Alice Chen\", {}),\n", - " (\"Frank Liu\", \"KNOWS\", \"James Wright\", {}),\n", - " (\"Eva Müller\", \"KNOWS\", \"Grace Okafor\", {}),\n", - " # RESEARCHES / WORKS_ON\n", - " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", - " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", - " (\"Grace Okafor\", \"RESEARCHES\", \"Knowledge Graph\", {\"since\": 2021}),\n", - " (\"Isla Nakamura\", \"RESEARCHES\", \"Graph Neural Network\", {\"since\": 2020}),\n", - " (\"Frank Liu\", \"RESEARCHES\", \"Graph Neural Network\", {}),\n", - " (\"Grace Okafor\", \"RESEARCHES\", \"GraphRAG\", {\"since\": 2023}),\n", - " # USES\n", - " (\"Synthex\", \"USES\", \"Knowledge Graph\", {}),\n", - " (\"Synthex\", \"USES\", \"Vector Database\", {}),\n", - " (\"Synthex\", \"USES\", \"RAG\", {}),\n", - " (\"OpenAI\", \"USES\", \"Transformer\", {}),\n", - " (\"Google\", \"USES\", \"Transformer\", {}),\n", - " # ACQUIRED\n", - " (\"Google\", \"ACQUIRED\", \"DeepMind\", {\"year\": 2014}),\n", - " # BUILDS_ON\n", - " (\"GraphRAG\", \"BUILDS_ON\", \"Knowledge Graph\", {}),\n", - " (\"GraphRAG\", \"BUILDS_ON\", \"RAG\", {}),\n", - " (\"RAG\", \"BUILDS_ON\", \"Vector Database\", {}),\n", - " (\"LLM\", \"BUILDS_ON\", \"Transformer\", {}),\n", - "]\n", - "\n", - "src_names = {p[\"name\"] for p in people}\n", - "tech_names = {t[\"name\"] for t in technologies}\n", - "company_names = {c[\"name\"] for c in companies}\n", - "\n", - "\n", - "def _label(name):\n", - " if name in src_names:\n", - " return \"Person\"\n", - " if name in tech_names:\n", - " return \"Technology\"\n", - " if name in company_names:\n", - " return \"Company\"\n", - " raise ValueError(f\"Unknown edge endpoint: {name!r}\")\n", - "\n", - "\n", - "for src, rel, dst, props in edges:\n", - " src_label = _label(src)\n", - " dst_label = _label(dst)\n", - " set_clause = \", \".join(f\"r.{k} = ${k}\" for k in props) if props else \"\"\n", - " set_part = f\" SET {set_clause}\" if set_clause else \"\"\n", - " client.cypher(\n", - " f\"MATCH (a:{src_label} {{name: $src}}) \"\n", - " f\"MATCH (b:{dst_label} {{name: $dst}}) \"\n", - " f\"MERGE (a)-[r:{rel}]->(b)\" + set_part,\n", - " params={\"src\": src, \"dst\": dst, **props},\n", - " )\n", - "\n", - "print(f\"Created {len(edges)} relationships\")" + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "s", + "r", + "c", + "\"", + ":", + " ", + "s", + "r", + "c", + ",", + " ", + "\"", + "d", + "s", + "t", + "\"", + ":", + " ", + "d", + "s", + "t", + ",", + " ", + "\"", + "t", + "a", + "g", + "\"", + ":", + " ", + "D", + "E", + "M", + "O", + "_", + "T", + "A", + "G", + ",", + " ", + "*", + "*", + "p", + "r", + "o", + "p", + "s", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "r", + "e", + "a", + "t", + "e", + "d", + " ", + "{", + "l", + "e", + "n", + "(", + "e", + "d", + "g", + "e", + "s", + ")", + "}", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + "s", + "h", + "i", + "p", + "s", + "\"", + ")" ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 735fdc1..298d321 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -40,54 +40,1967 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "# In Colab, build from source (~5 min first run, cached after).\n", - "# Locally, it installs in seconds if a pre-built wheel is cached.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"llama-index-graph-stores-coordinode\",\n", - " \"llama-index-core\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - ")\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "y", + "s", + ",", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + "\n", + "\n", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + " ", + "=", + " ", + "\"", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "l", + "a", + "b", + "\"", + " ", + "i", + "n", + " ", + "s", + "y", + "s", + ".", + "m", + "o", + "d", + "u", + "l", + "e", + "s", + "\n", + "\n", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "n", + " ", + "C", + "o", + "l", + "a", + "b", + " ", + "o", + "n", + "l", + "y", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + ")", + ".", + "\n", + "i", + "f", + " ", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + ":", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "R", + "u", + "s", + "t", + " ", + "t", + "o", + "o", + "l", + "c", + "h", + "a", + "i", + "n", + " ", + "v", + "i", + "a", + " ", + "r", + "u", + "s", + "t", + "u", + "p", + " ", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "C", + "o", + "l", + "a", + "b", + "'", + "s", + " ", + "a", + "p", + "t", + " ", + "p", + "a", + "c", + "k", + "a", + "g", + "e", + "s", + " ", + "s", + "h", + "i", + "p", + " ", + "r", + "u", + "s", + "t", + "c", + " ", + "≤", + "1", + ".", + "7", + "5", + ",", + " ", + "w", + "h", + "i", + "c", + "h", + " ", + "c", + "a", + "n", + "n", + "o", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "≥", + "1", + ".", + "8", + "0", + " ", + "f", + "o", + "r", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "y", + "o", + "3", + ")", + ".", + " ", + "a", + "p", + "t", + "-", + "g", + "e", + "t", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "a", + " ", + "v", + "i", + "a", + "b", + "l", + "e", + " ", + "a", + "l", + "t", + "e", + "r", + "n", + "a", + "t", + "i", + "v", + "e", + " ", + "h", + "e", + "r", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "D", + "o", + "w", + "n", + "l", + "o", + "a", + "d", + " ", + "t", + "h", + "e", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "r", + " ", + "t", + "o", + " ", + "a", + " ", + "t", + "e", + "m", + "p", + " ", + "f", + "i", + "l", + "e", + " ", + "a", + "n", + "d", + " ", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + " ", + "i", + "t", + " ", + "e", + "x", + "p", + "l", + "i", + "c", + "i", + "t", + "l", + "y", + " ", + "—", + " ", + "t", + "h", + "i", + "s", + " ", + "a", + "v", + "o", + "i", + "d", + "s", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "p", + "i", + "p", + "i", + "n", + "g", + " ", + "r", + "e", + "m", + "o", + "t", + "e", + " ", + "c", + "o", + "n", + "t", + "e", + "n", + "t", + " ", + "d", + "i", + "r", + "e", + "c", + "t", + "l", + "y", + " ", + "i", + "n", + "t", + "o", + " ", + "a", + " ", + "s", + "h", + "e", + "l", + "l", + " ", + "w", + "h", + "i", + "l", + "e", + " ", + "m", + "a", + "i", + "n", + "t", + "a", + "i", + "n", + "i", + "n", + "g", + " ", + "H", + "T", + "T", + "P", + "S", + "/", + "T", + "L", + "S", + " ", + "s", + "e", + "c", + "u", + "r", + "i", + "t", + "y", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "t", + "h", + "r", + "o", + "u", + "g", + "h", + " ", + "P", + "y", + "t", + "h", + "o", + "n", + "'", + "s", + " ", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + " ", + "s", + "s", + "l", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + " ", + "(", + "c", + "e", + "r", + "t", + "-", + "v", + "e", + "r", + "i", + "f", + "i", + "e", + "d", + ",", + " ", + "T", + "L", + "S", + " ", + "1", + ".", + "2", + "+", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "s", + "s", + "l", + " ", + "a", + "s", + " ", + "_", + "s", + "s", + "l", + ",", + " ", + "t", + "e", + "m", + "p", + "f", + "i", + "l", + "e", + " ", + "a", + "s", + " ", + "_", + "t", + "m", + "p", + ",", + " ", + "u", + "r", + "l", + "l", + "i", + "b", + ".", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + " ", + "a", + "s", + " ", + "_", + "u", + "r", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "t", + "x", + " ", + "=", + " ", + "_", + "s", + "s", + "l", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + "_", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "t", + "m", + "p", + ".", + "N", + "a", + "m", + "e", + "d", + "T", + "e", + "m", + "p", + "o", + "r", + "a", + "r", + "y", + "F", + "i", + "l", + "e", + "(", + "m", + "o", + "d", + "e", + "=", + "\"", + "w", + "b", + "\"", + ",", + " ", + "s", + "u", + "f", + "f", + "i", + "x", + "=", + "\"", + ".", + "s", + "h", + "\"", + ",", + " ", + "d", + "e", + "l", + "e", + "t", + "e", + "=", + "F", + "a", + "l", + "s", + "e", + ")", + " ", + "a", + "s", + " ", + "_", + "f", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "u", + "r", + ".", + "u", + "r", + "l", + "o", + "p", + "e", + "n", + "(", + "\"", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "s", + "h", + ".", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + "\"", + ",", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "=", + "_", + "c", + "t", + "x", + ")", + " ", + "a", + "s", + " ", + "_", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "f", + ".", + "w", + "r", + "i", + "t", + "e", + "(", + "_", + "r", + ".", + "r", + "e", + "a", + "d", + "(", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + " ", + "=", + " ", + "_", + "f", + ".", + "n", + "a", + "m", + "e", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "\"", + "/", + "b", + "i", + "n", + "/", + "s", + "h", + "\"", + ",", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ",", + " ", + "\"", + "-", + "s", + "\"", + ",", + " ", + "\"", + "-", + "-", + "\"", + ",", + " ", + "\"", + "-", + "y", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "f", + "i", + "n", + "a", + "l", + "l", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "u", + "n", + "l", + "i", + "n", + "k", + "(", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ")", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "d", + "d", + " ", + "c", + "a", + "r", + "g", + "o", + " ", + "t", + "o", + " ", + "P", + "A", + "T", + "H", + " ", + "s", + "o", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "i", + "p", + " ", + "c", + "a", + "n", + " ", + "f", + "i", + "n", + "d", + " ", + "i", + "t", + ".", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + " ", + "=", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "p", + "a", + "n", + "d", + "u", + "s", + "e", + "r", + "(", + "\"", + "~", + "/", + ".", + "c", + "a", + "r", + "g", + "o", + "/", + "b", + "i", + "n", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "P", + "A", + "T", + "H", + "\"", + "]", + " ", + "=", + " ", + "f", + "\"", + "{", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + "}", + "{", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + "s", + "e", + "p", + "}", + "{", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "'", + "P", + "A", + "T", + "H", + "'", + ",", + " ", + "'", + "'", + ")", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + " ", + "\"", + "-", + "m", + "\"", + ",", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + ",", + " ", + "\"", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "l", + "a", + "m", + "a", + "-", + "i", + "n", + "d", + "e", + "x", + "-", + "g", + "r", + "a", + "p", + "h", + "-", + "s", + "t", + "o", + "r", + "e", + "s", + "-", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "l", + "a", + "m", + "a", + "-", + "i", + "n", + "d", + "e", + "x", + "-", + "c", + "o", + "r", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + ")", + "\n", + "\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + "\n", "\n", - "import nest_asyncio\n", + "n", + "e", + "s", + "t", + "_", + "a", + "s", + "y", + "n", + "c", + "i", + "o", + ".", + "a", + "p", + "p", + "l", + "y", + "(", + ")", "\n", - "nest_asyncio.apply()\n", "\n", - "print(\"SDK installed\")" + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "S", + "D", + "K", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "d", + "\"", + ")" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 8c5ddf8..b4f70d0 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -37,48 +37,1937 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " ],\n", - " check=True,\n", - ")\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "y", + "s", + ",", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + "\n", + "\n", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + " ", + "=", + " ", + "\"", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "l", + "a", + "b", + "\"", + " ", + "i", + "n", + " ", + "s", + "y", + "s", + ".", + "m", + "o", + "d", + "u", + "l", + "e", + "s", + "\n", + "\n", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "n", + " ", + "C", + "o", + "l", + "a", + "b", + " ", + "o", + "n", + "l", + "y", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + ")", + ".", + "\n", + "i", + "f", + " ", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + ":", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "R", + "u", + "s", + "t", + " ", + "t", + "o", + "o", + "l", + "c", + "h", + "a", + "i", + "n", + " ", + "v", + "i", + "a", + " ", + "r", + "u", + "s", + "t", + "u", + "p", + " ", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "C", + "o", + "l", + "a", + "b", + "'", + "s", + " ", + "a", + "p", + "t", + " ", + "p", + "a", + "c", + "k", + "a", + "g", + "e", + "s", + " ", + "s", + "h", + "i", + "p", + " ", + "r", + "u", + "s", + "t", + "c", + " ", + "≤", + "1", + ".", + "7", + "5", + ",", + " ", + "w", + "h", + "i", + "c", + "h", + " ", + "c", + "a", + "n", + "n", + "o", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "≥", + "1", + ".", + "8", + "0", + " ", + "f", + "o", + "r", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "y", + "o", + "3", + ")", + ".", + " ", + "a", + "p", + "t", + "-", + "g", + "e", + "t", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "a", + " ", + "v", + "i", + "a", + "b", + "l", + "e", + " ", + "a", + "l", + "t", + "e", + "r", + "n", + "a", + "t", + "i", + "v", + "e", + " ", + "h", + "e", + "r", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "D", + "o", + "w", + "n", + "l", + "o", + "a", + "d", + " ", + "t", + "h", + "e", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "r", + " ", + "t", + "o", + " ", + "a", + " ", + "t", + "e", + "m", + "p", + " ", + "f", + "i", + "l", + "e", + " ", + "a", + "n", + "d", + " ", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + " ", + "i", + "t", + " ", + "e", + "x", + "p", + "l", + "i", + "c", + "i", + "t", + "l", + "y", + " ", + "—", + " ", + "t", + "h", + "i", + "s", + " ", + "a", + "v", + "o", + "i", + "d", + "s", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "p", + "i", + "p", + "i", + "n", + "g", + " ", + "r", + "e", + "m", + "o", + "t", + "e", + " ", + "c", + "o", + "n", + "t", + "e", + "n", + "t", + " ", + "d", + "i", + "r", + "e", + "c", + "t", + "l", + "y", + " ", + "i", + "n", + "t", + "o", + " ", + "a", + " ", + "s", + "h", + "e", + "l", + "l", + " ", + "w", + "h", + "i", + "l", + "e", + " ", + "m", + "a", + "i", + "n", + "t", + "a", + "i", + "n", + "i", + "n", + "g", + " ", + "H", + "T", + "T", + "P", + "S", + "/", + "T", + "L", + "S", + " ", + "s", + "e", + "c", + "u", + "r", + "i", + "t", + "y", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "t", + "h", + "r", + "o", + "u", + "g", + "h", + " ", + "P", + "y", + "t", + "h", + "o", + "n", + "'", + "s", + " ", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + " ", + "s", + "s", + "l", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + " ", + "(", + "c", + "e", + "r", + "t", + "-", + "v", + "e", + "r", + "i", + "f", + "i", + "e", + "d", + ",", + " ", + "T", + "L", + "S", + " ", + "1", + ".", + "2", + "+", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "s", + "s", + "l", + " ", + "a", + "s", + " ", + "_", + "s", + "s", + "l", + ",", + " ", + "t", + "e", + "m", + "p", + "f", + "i", + "l", + "e", + " ", + "a", + "s", + " ", + "_", + "t", + "m", + "p", + ",", + " ", + "u", + "r", + "l", + "l", + "i", + "b", + ".", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + " ", + "a", + "s", + " ", + "_", + "u", + "r", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "t", + "x", + " ", + "=", + " ", + "_", + "s", + "s", + "l", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + "_", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "t", + "m", + "p", + ".", + "N", + "a", + "m", + "e", + "d", + "T", + "e", + "m", + "p", + "o", + "r", + "a", + "r", + "y", + "F", + "i", + "l", + "e", + "(", + "m", + "o", + "d", + "e", + "=", + "\"", + "w", + "b", + "\"", + ",", + " ", + "s", + "u", + "f", + "f", + "i", + "x", + "=", + "\"", + ".", + "s", + "h", + "\"", + ",", + " ", + "d", + "e", + "l", + "e", + "t", + "e", + "=", + "F", + "a", + "l", + "s", + "e", + ")", + " ", + "a", + "s", + " ", + "_", + "f", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "u", + "r", + ".", + "u", + "r", + "l", + "o", + "p", + "e", + "n", + "(", + "\"", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "s", + "h", + ".", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + "\"", + ",", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "=", + "_", + "c", + "t", + "x", + ")", + " ", + "a", + "s", + " ", + "_", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "f", + ".", + "w", + "r", + "i", + "t", + "e", + "(", + "_", + "r", + ".", + "r", + "e", + "a", + "d", + "(", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + " ", + "=", + " ", + "_", + "f", + ".", + "n", + "a", + "m", + "e", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "\"", + "/", + "b", + "i", + "n", + "/", + "s", + "h", + "\"", + ",", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ",", + " ", + "\"", + "-", + "s", + "\"", + ",", + " ", + "\"", + "-", + "-", + "\"", + ",", + " ", + "\"", + "-", + "y", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "f", + "i", + "n", + "a", + "l", + "l", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "u", + "n", + "l", + "i", + "n", + "k", + "(", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ")", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "d", + "d", + " ", + "c", + "a", + "r", + "g", + "o", + " ", + "t", + "o", + " ", + "P", + "A", + "T", + "H", + " ", + "s", + "o", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "i", + "p", + " ", + "c", + "a", + "n", + " ", + "f", + "i", + "n", + "d", + " ", + "i", + "t", + ".", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + " ", + "=", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "p", + "a", + "n", + "d", + "u", + "s", + "e", + "r", + "(", + "\"", + "~", + "/", + ".", + "c", + "a", + "r", + "g", + "o", + "/", + "b", + "i", + "n", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "P", + "A", + "T", + "H", + "\"", + "]", + " ", + "=", + " ", + "f", + "\"", + "{", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + "}", + "{", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + "s", + "e", + "p", + "}", + "{", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "'", + "P", + "A", + "T", + "H", + "'", + ",", + " ", + "'", + "'", + ")", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + " ", + "\"", + "-", + "m", + "\"", + ",", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + ",", + " ", + "\"", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + ",", "\n", - "print(\"SDK installed\")" + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "c", + "o", + "m", + "m", + "u", + "n", + "i", + "t", + "y", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "o", + "p", + "e", + "n", + "a", + "i", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "S", + "D", + "K", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "d", + "\"", + ")" ] }, { @@ -337,21 +2226,563 @@ "metadata": {}, "outputs": [], "source": [ - "if not os.environ.get(\"OPENAI_API_KEY\"):\n", - " print(\n", - " 'Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.'\n", - " )\n", - "else:\n", - " from langchain.chains import GraphCypherQAChain\n", - " from langchain_openai import ChatOpenAI\n", - "\n", - " chain = GraphCypherQAChain.from_llm(\n", - " ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n", - " graph=graph,\n", - " verbose=True,\n", - " )\n", - " result = chain.invoke(\"Who influenced Shannon?\")\n", - " print(\"Answer:\", result[\"result\"])" + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + "\"", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "'", + "S", + "k", + "i", + "p", + "p", + "i", + "n", + "g", + ":", + " ", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "s", + "e", + "t", + ".", + " ", + "S", + "e", + "t", + " ", + "i", + "t", + " ", + "v", + "i", + "a", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + "\"", + "]", + " ", + "=", + " ", + "\"", + "s", + "k", + "-", + ".", + ".", + ".", + "\"", + " ", + "a", + "n", + "d", + " ", + "r", + "e", + "-", + "r", + "u", + "n", + " ", + "t", + "h", + "i", + "s", + " ", + "c", + "e", + "l", + "l", + ".", + "'", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "e", + "l", + "s", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + ".", + "c", + "h", + "a", + "i", + "n", + "s", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "G", + "r", + "a", + "p", + "h", + "C", + "y", + "p", + "h", + "e", + "r", + "Q", + "A", + "C", + "h", + "a", + "i", + "n", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "_", + "o", + "p", + "e", + "n", + "a", + "i", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "h", + "a", + "t", + "O", + "p", + "e", + "n", + "A", + "I", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "a", + "i", + "n", + " ", + "=", + " ", + "G", + "r", + "a", + "p", + "h", + "C", + "y", + "p", + "h", + "e", + "r", + "Q", + "A", + "C", + "h", + "a", + "i", + "n", + ".", + "f", + "r", + "o", + "m", + "_", + "l", + "l", + "m", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "C", + "h", + "a", + "t", + "O", + "p", + "e", + "n", + "A", + "I", + "(", + "m", + "o", + "d", + "e", + "l", + "=", + "\"", + "g", + "p", + "t", + "-", + "4", + "o", + "-", + "m", + "i", + "n", + "i", + "\"", + ",", + " ", + "t", + "e", + "m", + "p", + "e", + "r", + "a", + "t", + "u", + "r", + "e", + "=", + "0", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "g", + "r", + "a", + "p", + "h", + "=", + "g", + "r", + "a", + "p", + "h", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "v", + "e", + "r", + "b", + "o", + "s", + "e", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "a", + "l", + "l", + "o", + "w", + "_", + "d", + "a", + "n", + "g", + "e", + "r", + "o", + "u", + "s", + "_", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + "s", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "s", + "u", + "l", + "t", + " ", + "=", + " ", + "c", + "h", + "a", + "i", + "n", + ".", + "i", + "n", + "v", + "o", + "k", + "e", + "(", + "\"", + "W", + "h", + "o", + " ", + "i", + "n", + "f", + "l", + "u", + "e", + "n", + "c", + "e", + "d", + " ", + "S", + "h", + "a", + "n", + "n", + "o", + "n", + "?", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "A", + "n", + "s", + "w", + "e", + "r", + ":", + "\"", + ",", + " ", + "r", + "e", + "s", + "u", + "l", + "t", + "[", + "\"", + "r", + "e", + "s", + "u", + "l", + "t", + "\"", + "]", + ")" ] }, { diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index a6cb8e1..c0a29cf 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -38,49 +38,1937 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Always install coordinode-embedded so it's available as a local fallback.\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True)\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " \"langgraph\",\n", - " ],\n", - " check=True,\n", - ")\n", - "\n", - "print(\"SDK installed\")" + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "y", + "s", + ",", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + "\n", + "\n", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + " ", + "=", + " ", + "\"", + "g", + "o", + "o", + "g", + "l", + "e", + ".", + "c", + "o", + "l", + "a", + "b", + "\"", + " ", + "i", + "n", + " ", + "s", + "y", + "s", + ".", + "m", + "o", + "d", + "u", + "l", + "e", + "s", + "\n", + "\n", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "n", + " ", + "C", + "o", + "l", + "a", + "b", + " ", + "o", + "n", + "l", + "y", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + ")", + ".", + "\n", + "i", + "f", + " ", + "I", + "N", + "_", + "C", + "O", + "L", + "A", + "B", + ":", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "I", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "R", + "u", + "s", + "t", + " ", + "t", + "o", + "o", + "l", + "c", + "h", + "a", + "i", + "n", + " ", + "v", + "i", + "a", + " ", + "r", + "u", + "s", + "t", + "u", + "p", + " ", + "(", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "C", + "o", + "l", + "a", + "b", + "'", + "s", + " ", + "a", + "p", + "t", + " ", + "p", + "a", + "c", + "k", + "a", + "g", + "e", + "s", + " ", + "s", + "h", + "i", + "p", + " ", + "r", + "u", + "s", + "t", + "c", + " ", + "≤", + "1", + ".", + "7", + "5", + ",", + " ", + "w", + "h", + "i", + "c", + "h", + " ", + "c", + "a", + "n", + "n", + "o", + "t", + " ", + "b", + "u", + "i", + "l", + "d", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "(", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + "s", + " ", + "R", + "u", + "s", + "t", + " ", + "≥", + "1", + ".", + "8", + "0", + " ", + "f", + "o", + "r", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "y", + "o", + "3", + ")", + ".", + " ", + "a", + "p", + "t", + "-", + "g", + "e", + "t", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "a", + " ", + "v", + "i", + "a", + "b", + "l", + "e", + " ", + "a", + "l", + "t", + "e", + "r", + "n", + "a", + "t", + "i", + "v", + "e", + " ", + "h", + "e", + "r", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "D", + "o", + "w", + "n", + "l", + "o", + "a", + "d", + " ", + "t", + "h", + "e", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "r", + " ", + "t", + "o", + " ", + "a", + " ", + "t", + "e", + "m", + "p", + " ", + "f", + "i", + "l", + "e", + " ", + "a", + "n", + "d", + " ", + "e", + "x", + "e", + "c", + "u", + "t", + "e", + " ", + "i", + "t", + " ", + "e", + "x", + "p", + "l", + "i", + "c", + "i", + "t", + "l", + "y", + " ", + "—", + " ", + "t", + "h", + "i", + "s", + " ", + "a", + "v", + "o", + "i", + "d", + "s", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "p", + "i", + "p", + "i", + "n", + "g", + " ", + "r", + "e", + "m", + "o", + "t", + "e", + " ", + "c", + "o", + "n", + "t", + "e", + "n", + "t", + " ", + "d", + "i", + "r", + "e", + "c", + "t", + "l", + "y", + " ", + "i", + "n", + "t", + "o", + " ", + "a", + " ", + "s", + "h", + "e", + "l", + "l", + " ", + "w", + "h", + "i", + "l", + "e", + " ", + "m", + "a", + "i", + "n", + "t", + "a", + "i", + "n", + "i", + "n", + "g", + " ", + "H", + "T", + "T", + "P", + "S", + "/", + "T", + "L", + "S", + " ", + "s", + "e", + "c", + "u", + "r", + "i", + "t", + "y", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "t", + "h", + "r", + "o", + "u", + "g", + "h", + " ", + "P", + "y", + "t", + "h", + "o", + "n", + "'", + "s", + " ", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + " ", + "s", + "s", + "l", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + " ", + "(", + "c", + "e", + "r", + "t", + "-", + "v", + "e", + "r", + "i", + "f", + "i", + "e", + "d", + ",", + " ", + "T", + "L", + "S", + " ", + "1", + ".", + "2", + "+", + ")", + ".", + "\n", + " ", + " ", + " ", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "s", + "s", + "l", + " ", + "a", + "s", + " ", + "_", + "s", + "s", + "l", + ",", + " ", + "t", + "e", + "m", + "p", + "f", + "i", + "l", + "e", + " ", + "a", + "s", + " ", + "_", + "t", + "m", + "p", + ",", + " ", + "u", + "r", + "l", + "l", + "i", + "b", + ".", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + " ", + "a", + "s", + " ", + "_", + "u", + "r", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "t", + "x", + " ", + "=", + " ", + "_", + "s", + "s", + "l", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "d", + "e", + "f", + "a", + "u", + "l", + "t", + "_", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "t", + "m", + "p", + ".", + "N", + "a", + "m", + "e", + "d", + "T", + "e", + "m", + "p", + "o", + "r", + "a", + "r", + "y", + "F", + "i", + "l", + "e", + "(", + "m", + "o", + "d", + "e", + "=", + "\"", + "w", + "b", + "\"", + ",", + " ", + "s", + "u", + "f", + "f", + "i", + "x", + "=", + "\"", + ".", + "s", + "h", + "\"", + ",", + " ", + "d", + "e", + "l", + "e", + "t", + "e", + "=", + "F", + "a", + "l", + "s", + "e", + ")", + " ", + "a", + "s", + " ", + "_", + "f", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "_", + "u", + "r", + ".", + "u", + "r", + "l", + "o", + "p", + "e", + "n", + "(", + "\"", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "s", + "h", + ".", + "r", + "u", + "s", + "t", + "u", + "p", + ".", + "r", + "s", + "\"", + ",", + " ", + "c", + "o", + "n", + "t", + "e", + "x", + "t", + "=", + "_", + "c", + "t", + "x", + ")", + " ", + "a", + "s", + " ", + "_", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "f", + ".", + "w", + "r", + "i", + "t", + "e", + "(", + "_", + "r", + ".", + "r", + "e", + "a", + "d", + "(", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + " ", + "=", + " ", + "_", + "f", + ".", + "n", + "a", + "m", + "e", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "\"", + "/", + "b", + "i", + "n", + "/", + "s", + "h", + "\"", + ",", + " ", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ",", + " ", + "\"", + "-", + "s", + "\"", + ",", + " ", + "\"", + "-", + "-", + "\"", + ",", + " ", + "\"", + "-", + "y", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "f", + "i", + "n", + "a", + "l", + "l", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "u", + "n", + "l", + "i", + "n", + "k", + "(", + "_", + "r", + "u", + "s", + "t", + "u", + "p", + "_", + "p", + "a", + "t", + "h", + ")", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "d", + "d", + " ", + "c", + "a", + "r", + "g", + "o", + " ", + "t", + "o", + " ", + "P", + "A", + "T", + "H", + " ", + "s", + "o", + " ", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "/", + "p", + "i", + "p", + " ", + "c", + "a", + "n", + " ", + "f", + "i", + "n", + "d", + " ", + "i", + "t", + ".", + "\n", + " ", + " ", + " ", + " ", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + " ", + "=", + " ", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + ".", + "e", + "x", + "p", + "a", + "n", + "d", + "u", + "s", + "e", + "r", + "(", + "\"", + "~", + "/", + ".", + "c", + "a", + "r", + "g", + "o", + "/", + "b", + "i", + "n", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "P", + "A", + "T", + "H", + "\"", + "]", + " ", + "=", + " ", + "f", + "\"", + "{", + "_", + "c", + "a", + "r", + "g", + "o", + "_", + "b", + "i", + "n", + "}", + "{", + "o", + "s", + ".", + "p", + "a", + "t", + "h", + "s", + "e", + "p", + "}", + "{", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "'", + "P", + "A", + "T", + "H", + "'", + ",", + " ", + "'", + "'", + ")", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "[", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + " ", + "\"", + "-", + "m", + "\"", + ",", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + " ", + "\"", + "-", + "q", + "\"", + ",", + " ", + "\"", + "m", + "a", + "t", + "u", + "r", + "i", + "n", + "\"", + "]", + ",", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ")", + "\n", + " ", + " ", + " ", + " ", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "\n", + "s", + "u", + "b", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ".", + "r", + "u", + "n", + "(", + "\n", + " ", + " ", + " ", + " ", + "[", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "s", + "y", + "s", + ".", + "e", + "x", + "e", + "c", + "u", + "t", + "a", + "b", + "l", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "m", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "p", + "i", + "p", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "-", + "q", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "c", + "o", + "m", + "m", + "u", + "n", + "i", + "t", + "y", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "-", + "o", + "p", + "e", + "n", + "a", + "i", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "l", + "a", + "n", + "g", + "g", + "r", + "a", + "p", + "h", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "]", + ",", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "e", + "c", + "k", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + ")", + "\n", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "S", + "D", + "K", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "d", + "\"", + ")" ] }, { @@ -152,91 +2040,3667 @@ "metadata": {}, "outputs": [], "source": [ - "import os, re, uuid\n", - "from langchain_core.tools import tool\n", - "\n", - "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", - "\n", - "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", - "# Regex guards for query_facts (demo safety guard).\n", - "_WRITE_CLAUSE_RE = re.compile(\n", - " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", - " re.IGNORECASE,\n", - ")\n", - "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", - "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", - "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", - "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", - "# In production code, use server-side row-level security instead of client regex.\n", - "_SESSION_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", - " re.IGNORECASE,\n", - ")\n", - "\n", - "\n", - "@tool\n", - "def save_fact(subject: str, relation: str, obj: str) -> str:\n", - " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", - " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", - " rel_type = relation.upper().replace(\" \", \"_\")\n", - " # Validate rel_type before interpolating into Cypher to prevent injection.\n", - " if not _REL_TYPE_RE.fullmatch(rel_type):\n", - " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", - " client.cypher(\n", - " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", - " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", - " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", - " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", - " )\n", - " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", - "\n", - "\n", - "@tool\n", - "def query_facts(cypher: str) -> str:\n", - " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", - " Must scope reads via either WHERE .session = $sess\n", - " or a node pattern {session: $sess}.\"\"\"\n", - " q = cypher.strip()\n", - " if _WRITE_CLAUSE_RE.search(q):\n", - " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", - " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", - " if not _SESSION_SCOPE_RE.search(q):\n", - " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", - " rows = client.cypher(q, params={\"sess\": SESSION})\n", - " return str(rows[:20]) if rows else \"No results\"\n", - "\n", - "\n", - "@tool\n", - "def find_related(entity_name: str, depth: int = 1) -> str:\n", - " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", - " safe_depth = max(1, min(int(depth), 3))\n", - " rows = client.cypher(\n", - " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m) \"\n", - " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", - " params={\"name\": entity_name, \"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return f\"No related entities found for {entity_name}\"\n", - " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", - "\n", - "\n", - "@tool\n", - "def list_all_facts() -> str:\n", - " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", - " rows = client.cypher(\n", - " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", - " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", - " params={\"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return \"No facts stored yet\"\n", - " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "r", + "e", + ",", + " ", + "u", + "u", + "i", + "d", + "\n", + "f", + "r", + "o", + "m", + " ", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "_", + "c", + "o", + "r", + "e", + ".", + "t", + "o", + "o", + "l", + "s", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "t", + "o", + "o", + "l", + "\n", + "\n", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + " ", + "=", + " ", + "u", + "u", + "i", + "d", + ".", + "u", + "u", + "i", + "d", + "4", + "(", + ")", + ".", + "h", + "e", + "x", + "[", + ":", + "8", + "]", + " ", + " ", + "#", + " ", + "i", + "s", + "o", + "l", + "a", + "t", + "e", + "s", + " ", + "t", + "h", + "i", + "s", + " ", + "d", + "e", + "m", + "o", + "'", + "s", + " ", + "d", + "a", + "t", + "a", + " ", + "f", + "r", + "o", + "m", + " ", + "o", + "t", + "h", + "e", + "r", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + "s", + "\n", + "\n", + "_", + "R", + "E", + "L", + "_", + "T", + "Y", + "P", + "E", + "_", + "R", + "E", + " ", + "=", + " ", + "r", + "e", + ".", + "c", + "o", + "m", + "p", + "i", + "l", + "e", + "(", + "r", + "\"", + "[", + "A", + "-", + "Z", + "_", + "]", + "[", + "A", + "-", + "Z", + "0", + "-", + "9", + "_", + "]", + "*", + "\"", + ")", + "\n", + "#", + " ", + "R", + "e", + "g", + "e", + "x", + " ", + "g", + "u", + "a", + "r", + "d", + "s", + " ", + "f", + "o", + "r", + " ", + "q", + "u", + "e", + "r", + "y", + "_", + "f", + "a", + "c", + "t", + "s", + " ", + "(", + "d", + "e", + "m", + "o", + " ", + "s", + "a", + "f", + "e", + "t", + "y", + " ", + "g", + "u", + "a", + "r", + "d", + ")", + ".", + "\n", + "_", + "W", + "R", + "I", + "T", + "E", + "_", + "C", + "L", + "A", + "U", + "S", + "E", + "_", + "R", + "E", + " ", + "=", + " ", + "r", + "e", + ".", + "c", + "o", + "m", + "p", + "i", + "l", + "e", + "(", + "\n", + " ", + " ", + " ", + " ", + "r", + "\"", + "\\", + "b", + "(", + "C", + "R", + "E", + "A", + "T", + "E", + "|", + "M", + "E", + "R", + "G", + "E", + "|", + "D", + "E", + "L", + "E", + "T", + "E", + "|", + "D", + "E", + "T", + "A", + "C", + "H", + "|", + "S", + "E", + "T", + "|", + "R", + "E", + "M", + "O", + "V", + "E", + "|", + "D", + "R", + "O", + "P", + "|", + "C", + "A", + "L", + "L", + "|", + "L", + "O", + "A", + "D", + ")", + "\\", + "b", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + ".", + "I", + "G", + "N", + "O", + "R", + "E", + "C", + "A", + "S", + "E", + ",", + "\n", + ")", + "\n", + "#", + " ", + "N", + "O", + "T", + "E", + ":", + " ", + "t", + "h", + "i", + "s", + " ", + "g", + "u", + "a", + "r", + "d", + " ", + "c", + "h", + "e", + "c", + "k", + "s", + " ", + "t", + "h", + "a", + "t", + " ", + "A", + "T", + " ", + "L", + "E", + "A", + "S", + "T", + " ", + "O", + "N", + "E", + " ", + "n", + "o", + "d", + "e", + " ", + "p", + "a", + "t", + "t", + "e", + "r", + "n", + " ", + "c", + "a", + "r", + "r", + "i", + "e", + "s", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + " ", + "s", + "c", + "o", + "p", + "e", + ".", + "\n", + "#", + " ", + "A", + " ", + "C", + "a", + "r", + "t", + "e", + "s", + "i", + "a", + "n", + "-", + "p", + "r", + "o", + "d", + "u", + "c", + "t", + " ", + "q", + "u", + "e", + "r", + "y", + " ", + "s", + "u", + "c", + "h", + " ", + "a", + "s", + " ", + "`", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + ")", + ",", + " ", + "(", + "m", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + ")", + " ", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "n", + "`", "\n", + "#", + " ", + "w", + "o", + "u", + "l", + "d", + " ", + "p", + "a", + "s", + "s", + " ", + "y", + "e", + "t", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "u", + "n", + "s", + "c", + "o", + "p", + "e", + "d", + " ", + "r", + "o", + "w", + "s", + " ", + "f", + "o", + "r", + " ", + "`", + "n", + "`", + ".", + " ", + " ", + "A", + " ", + "c", + "o", + "m", + "p", + "l", + "e", + "t", + "e", + " ", + "p", + "e", + "r", + "-", + "a", + "l", + "i", + "a", + "s", + " ", + "c", + "h", + "e", + "c", + "k", + " ", + "w", + "o", + "u", + "l", + "d", "\n", - "tools = [save_fact, query_facts, find_related, list_all_facts]\n", - "print(f\"Session: {SESSION}\")\n", - "print(\"Tools:\", [t.name for t in tools])" + "#", + " ", + "r", + "e", + "q", + "u", + "i", + "r", + "e", + " ", + "p", + "a", + "r", + "s", + "i", + "n", + "g", + " ", + "t", + "h", + "e", + " ", + "C", + "y", + "p", + "h", + "e", + "r", + " ", + "A", + "S", + "T", + ",", + " ", + "w", + "h", + "i", + "c", + "h", + " ", + "i", + "s", + " ", + "o", + "u", + "t", + " ", + "o", + "f", + " ", + "s", + "c", + "o", + "p", + "e", + " ", + "f", + "o", + "r", + " ", + "a", + " ", + "d", + "e", + "m", + "o", + " ", + "s", + "a", + "f", + "e", + "t", + "y", + " ", + "g", + "u", + "a", + "r", + "d", + ".", + "\n", + "#", + " ", + "I", + "n", + " ", + "p", + "r", + "o", + "d", + "u", + "c", + "t", + "i", + "o", + "n", + " ", + "c", + "o", + "d", + "e", + ",", + " ", + "u", + "s", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + "-", + "s", + "i", + "d", + "e", + " ", + "r", + "o", + "w", + "-", + "l", + "e", + "v", + "e", + "l", + " ", + "s", + "e", + "c", + "u", + "r", + "i", + "t", + "y", + " ", + "i", + "n", + "s", + "t", + "e", + "a", + "d", + " ", + "o", + "f", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "r", + "e", + "g", + "e", + "x", + ".", + "\n", + "_", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "_", + "S", + "C", + "O", + "P", + "E", + "_", + "R", + "E", + " ", + "=", + " ", + "r", + "e", + ".", + "c", + "o", + "m", + "p", + "i", + "l", + "e", + "(", + "\n", + " ", + " ", + " ", + " ", + "r", + "\"", + "W", + "H", + "E", + "R", + "E", + "\\", + "b", + "[", + "^", + ";", + "{", + "}", + "]", + "*", + "\\", + ".", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + "\\", + "s", + "*", + "=", + "\\", + "s", + "*", + "\\", + "$", + "s", + "e", + "s", + "s", + "|", + "\\", + "{", + "[", + "^", + "}", + "]", + "*", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + "\\", + "s", + "*", + ":", + "\\", + "s", + "*", + "\\", + "$", + "s", + "e", + "s", + "s", + "[", + "^", + "}", + "]", + "*", + "\\", + "}", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + ".", + "I", + "G", + "N", + "O", + "R", + "E", + "C", + "A", + "S", + "E", + ",", + "\n", + ")", + "\n", + "\n", + "\n", + "@", + "t", + "o", + "o", + "l", + "\n", + "d", + "e", + "f", + " ", + "s", + "a", + "v", + "e", + "_", + "f", + "a", + "c", + "t", + "(", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + ":", + " ", + "s", + "t", + "r", + ",", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + ":", + " ", + "s", + "t", + "r", + ",", + " ", + "o", + "b", + "j", + ":", + " ", + "s", + "t", + "r", + ")", + " ", + "-", + ">", + " ", + "s", + "t", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "S", + "a", + "v", + "e", + " ", + "a", + " ", + "f", + "a", + "c", + "t", + " ", + "(", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + " ", + "→", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + " ", + "→", + " ", + "o", + "b", + "j", + "e", + "c", + "t", + ")", + " ", + "i", + "n", + "t", + "o", + " ", + "t", + "h", + "e", + " ", + "k", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "g", + "r", + "a", + "p", + "h", + ".", + "\n", + " ", + " ", + " ", + " ", + "E", + "x", + "a", + "m", + "p", + "l", + "e", + ":", + " ", + "s", + "a", + "v", + "e", + "_", + "f", + "a", + "c", + "t", + "(", + "'", + "A", + "l", + "i", + "c", + "e", + "'", + ",", + " ", + "'", + "W", + "O", + "R", + "K", + "S", + "_", + "A", + "T", + "'", + ",", + " ", + "'", + "A", + "c", + "m", + "e", + " ", + "C", + "o", + "r", + "p", + "'", + ")", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "l", + "_", + "t", + "y", + "p", + "e", + " ", + "=", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + ".", + "u", + "p", + "p", + "e", + "r", + "(", + ")", + ".", + "r", + "e", + "p", + "l", + "a", + "c", + "e", + "(", + "\"", + " ", + "\"", + ",", + " ", + "\"", + "_", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "V", + "a", + "l", + "i", + "d", + "a", + "t", + "e", + " ", + "r", + "e", + "l", + "_", + "t", + "y", + "p", + "e", + " ", + "b", + "e", + "f", + "o", + "r", + "e", + " ", + "i", + "n", + "t", + "e", + "r", + "p", + "o", + "l", + "a", + "t", + "i", + "n", + "g", + " ", + "i", + "n", + "t", + "o", + " ", + "C", + "y", + "p", + "h", + "e", + "r", + " ", + "t", + "o", + " ", + "p", + "r", + "e", + "v", + "e", + "n", + "t", + " ", + "i", + "n", + "j", + "e", + "c", + "t", + "i", + "o", + "n", + ".", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "_", + "R", + "E", + "L", + "_", + "T", + "Y", + "P", + "E", + "_", + "R", + "E", + ".", + "f", + "u", + "l", + "l", + "m", + "a", + "t", + "c", + "h", + "(", + "r", + "e", + "l", + "_", + "t", + "y", + "p", + "e", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "f", + "\"", + "I", + "n", + "v", + "a", + "l", + "i", + "d", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + " ", + "t", + "y", + "p", + "e", + " ", + "{", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + "!", + "r", + "}", + ":", + " ", + "o", + "n", + "l", + "y", + " ", + "l", + "e", + "t", + "t", + "e", + "r", + "s", + ",", + " ", + "d", + "i", + "g", + "i", + "t", + "s", + ",", + " ", + "a", + "n", + "d", + " ", + "u", + "n", + "d", + "e", + "r", + "s", + "c", + "o", + "r", + "e", + "s", + " ", + "a", + "l", + "l", + "o", + "w", + "e", + "d", + "\"", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "a", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "s", + ",", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "b", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "o", + ",", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "E", + "R", + "G", + "E", + " ", + "(", + "a", + ")", + "-", + "[", + "r", + ":", + "{", + "r", + "e", + "l", + "_", + "t", + "y", + "p", + "e", + "}", + "]", + "-", + ">", + "(", + "b", + ")", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "s", + "\"", + ":", + " ", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + ",", + " ", + "\"", + "o", + "\"", + ":", + " ", + "o", + "b", + "j", + ",", + " ", + "\"", + "s", + "e", + "s", + "s", + "\"", + ":", + " ", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "f", + "\"", + "S", + "a", + "v", + "e", + "d", + ":", + " ", + "{", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + "}", + " ", + "-", + "[", + "{", + "r", + "e", + "l", + "_", + "t", + "y", + "p", + "e", + "}", + "]", + "-", + ">", + " ", + "{", + "o", + "b", + "j", + "}", + "\"", + "\n", + "\n", + "\n", + "@", + "t", + "o", + "o", + "l", + "\n", + "d", + "e", + "f", + " ", + "q", + "u", + "e", + "r", + "y", + "_", + "f", + "a", + "c", + "t", + "s", + "(", + "c", + "y", + "p", + "h", + "e", + "r", + ":", + " ", + "s", + "t", + "r", + ")", + " ", + "-", + ">", + " ", + "s", + "t", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "R", + "u", + "n", + " ", + "a", + " ", + "r", + "e", + "a", + "d", + "-", + "o", + "n", + "l", + "y", + " ", + "C", + "y", + "p", + "h", + "e", + "r", + " ", + "M", + "A", + "T", + "C", + "H", + " ", + "q", + "u", + "e", + "r", + "y", + " ", + "a", + "g", + "a", + "i", + "n", + "s", + "t", + " ", + "t", + "h", + "e", + " ", + "k", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "g", + "r", + "a", + "p", + "h", + ".", + "\n", + " ", + " ", + " ", + " ", + "M", + "u", + "s", + "t", + " ", + "s", + "c", + "o", + "p", + "e", + " ", + "r", + "e", + "a", + "d", + "s", + " ", + "v", + "i", + "a", + " ", + "e", + "i", + "t", + "h", + "e", + "r", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "<", + "a", + "l", + "i", + "a", + "s", + ">", + ".", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + " ", + "=", + " ", + "$", + "s", + "e", + "s", + "s", + "\n", + " ", + " ", + " ", + " ", + "o", + "r", + " ", + "a", + " ", + "n", + "o", + "d", + "e", + " ", + "p", + "a", + "t", + "t", + "e", + "r", + "n", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "q", + " ", + "=", + " ", + "c", + "y", + "p", + "h", + "e", + "r", + ".", + "s", + "t", + "r", + "i", + "p", + "(", + ")", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "_", + "W", + "R", + "I", + "T", + "E", + "_", + "C", + "L", + "A", + "U", + "S", + "E", + "_", + "R", + "E", + ".", + "s", + "e", + "a", + "r", + "c", + "h", + "(", + "q", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "O", + "n", + "l", + "y", + " ", + "r", + "e", + "a", + "d", + "-", + "o", + "n", + "l", + "y", + " ", + "C", + "y", + "p", + "h", + "e", + "r", + " ", + "i", + "s", + " ", + "a", + "l", + "l", + "o", + "w", + "e", + "d", + " ", + "i", + "n", + " ", + "q", + "u", + "e", + "r", + "y", + "_", + "f", + "a", + "c", + "t", + "s", + ".", + "\"", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "R", + "e", + "q", + "u", + "i", + "r", + "e", + " ", + "$", + "s", + "e", + "s", + "s", + " ", + "i", + "n", + " ", + "a", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "c", + "l", + "a", + "u", + "s", + "e", + " ", + "o", + "r", + " ", + "n", + "o", + "d", + "e", + " ", + "p", + "a", + "t", + "t", + "e", + "r", + "n", + ",", + " ", + "n", + "o", + "t", + " ", + "j", + "u", + "s", + "t", + " ", + "a", + "n", + "y", + "w", + "h", + "e", + "r", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "A", + "c", + "c", + "e", + "p", + "t", + "s", + " ", + "b", + "o", + "t", + "h", + ":", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "n", + ".", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + " ", + "=", + " ", + "$", + "s", + "e", + "s", + "s", + " ", + " ", + "a", + "n", + "d", + " ", + " ", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + ")", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "_", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "_", + "S", + "C", + "O", + "P", + "E", + "_", + "R", + "E", + ".", + "s", + "e", + "a", + "r", + "c", + "h", + "(", + "q", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "Q", + "u", + "e", + "r", + "y", + " ", + "m", + "u", + "s", + "t", + " ", + "s", + "c", + "o", + "p", + "e", + " ", + "r", + "e", + "a", + "d", + "s", + " ", + "t", + "o", + " ", + "t", + "h", + "e", + " ", + "c", + "u", + "r", + "r", + "e", + "n", + "t", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + " ", + "w", + "i", + "t", + "h", + " ", + "e", + "i", + "t", + "h", + "e", + "r", + " ", + "W", + "H", + "E", + "R", + "E", + " ", + "<", + "a", + "l", + "i", + "a", + "s", + ">", + ".", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + " ", + "=", + " ", + "$", + "s", + "e", + "s", + "s", + " ", + "o", + "r", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "o", + "w", + "s", + " ", + "=", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "q", + ",", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "s", + "e", + "s", + "s", + "\"", + ":", + " ", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "}", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "s", + "t", + "r", + "(", + "r", + "o", + "w", + "s", + "[", + ":", + "2", + "0", + "]", + ")", + " ", + "i", + "f", + " ", + "r", + "o", + "w", + "s", + " ", + "e", + "l", + "s", + "e", + " ", + "\"", + "N", + "o", + " ", + "r", + "e", + "s", + "u", + "l", + "t", + "s", + "\"", + "\n", + "\n", + "\n", + "@", + "t", + "o", + "o", + "l", + "\n", + "d", + "e", + "f", + " ", + "f", + "i", + "n", + "d", + "_", + "r", + "e", + "l", + "a", + "t", + "e", + "d", + "(", + "e", + "n", + "t", + "i", + "t", + "y", + "_", + "n", + "a", + "m", + "e", + ":", + " ", + "s", + "t", + "r", + ",", + " ", + "d", + "e", + "p", + "t", + "h", + ":", + " ", + "i", + "n", + "t", + " ", + "=", + " ", + "1", + ")", + " ", + "-", + ">", + " ", + "s", + "t", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "F", + "i", + "n", + "d", + " ", + "a", + "l", + "l", + " ", + "e", + "n", + "t", + "i", + "t", + "i", + "e", + "s", + " ", + "r", + "e", + "a", + "c", + "h", + "a", + "b", + "l", + "e", + " ", + "f", + "r", + "o", + "m", + " ", + "e", + "n", + "t", + "i", + "t", + "y", + "_", + "n", + "a", + "m", + "e", + " ", + "w", + "i", + "t", + "h", + "i", + "n", + " ", + "t", + "h", + "e", + " ", + "g", + "i", + "v", + "e", + "n", + " ", + "n", + "u", + "m", + "b", + "e", + "r", + " ", + "o", + "f", + " ", + "h", + "o", + "p", + "s", + " ", + "(", + "m", + "a", + "x", + " ", + "3", + ")", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "s", + "a", + "f", + "e", + "_", + "d", + "e", + "p", + "t", + "h", + " ", + "=", + " ", + "m", + "a", + "x", + "(", + "1", + ",", + " ", + "m", + "i", + "n", + "(", + "i", + "n", + "t", + "(", + "d", + "e", + "p", + "t", + "h", + ")", + ",", + " ", + "3", + ")", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "o", + "w", + "s", + " ", + "=", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "n", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "{", + "n", + "a", + "m", + "e", + ":", + " ", + "$", + "n", + "a", + "m", + "e", + ",", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + "}", + ")", + "-", + "[", + "r", + "*", + "1", + ".", + ".", + "{", + "s", + "a", + "f", + "e", + "_", + "d", + "e", + "p", + "t", + "h", + "}", + "]", + "-", + ">", + "(", + "m", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "m", + ".", + "n", + "a", + "m", + "e", + " ", + "A", + "S", + " ", + "r", + "e", + "l", + "a", + "t", + "e", + "d", + ",", + " ", + "t", + "y", + "p", + "e", + "(", + "l", + "a", + "s", + "t", + "(", + "r", + ")", + ")", + " ", + "A", + "S", + " ", + "v", + "i", + "a", + " ", + "L", + "I", + "M", + "I", + "T", + " ", + "2", + "0", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "n", + "a", + "m", + "e", + "\"", + ":", + " ", + "e", + "n", + "t", + "i", + "t", + "y", + "_", + "n", + "a", + "m", + "e", + ",", + " ", + "\"", + "s", + "e", + "s", + "s", + "\"", + ":", + " ", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "r", + "o", + "w", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "f", + "\"", + "N", + "o", + " ", + "r", + "e", + "l", + "a", + "t", + "e", + "d", + " ", + "e", + "n", + "t", + "i", + "t", + "i", + "e", + "s", + " ", + "f", + "o", + "u", + "n", + "d", + " ", + "f", + "o", + "r", + " ", + "{", + "e", + "n", + "t", + "i", + "t", + "y", + "_", + "n", + "a", + "m", + "e", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "\\", + "n", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "f", + "\"", + "{", + "r", + "[", + "'", + "v", + "i", + "a", + "'", + "]", + "}", + " ", + "-", + ">", + " ", + "{", + "r", + "[", + "'", + "r", + "e", + "l", + "a", + "t", + "e", + "d", + "'", + "]", + "}", + "\"", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "r", + "o", + "w", + "s", + ")", + "\n", + "\n", + "\n", + "@", + "t", + "o", + "o", + "l", + "\n", + "d", + "e", + "f", + " ", + "l", + "i", + "s", + "t", + "_", + "a", + "l", + "l", + "_", + "f", + "a", + "c", + "t", + "s", + "(", + ")", + " ", + "-", + ">", + " ", + "s", + "t", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + "\"", + "\"", + "\"", + "L", + "i", + "s", + "t", + " ", + "e", + "v", + "e", + "r", + "y", + " ", + "f", + "a", + "c", + "t", + " ", + "s", + "t", + "o", + "r", + "e", + "d", + " ", + "i", + "n", + " ", + "t", + "h", + "e", + " ", + "c", + "u", + "r", + "r", + "e", + "n", + "t", + " ", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + "'", + "s", + " ", + "k", + "n", + "o", + "w", + "l", + "e", + "d", + "g", + "e", + " ", + "g", + "r", + "a", + "p", + "h", + ".", + "\"", + "\"", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "o", + "w", + "s", + " ", + "=", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + ".", + "c", + "y", + "p", + "h", + "e", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "M", + "A", + "T", + "C", + "H", + " ", + "(", + "a", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + ")", + "-", + "[", + "r", + "]", + "-", + ">", + "(", + "b", + ":", + "E", + "n", + "t", + "i", + "t", + "y", + " ", + "{", + "s", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "$", + "s", + "e", + "s", + "s", + "}", + ")", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "R", + "E", + "T", + "U", + "R", + "N", + " ", + "a", + ".", + "n", + "a", + "m", + "e", + " ", + "A", + "S", + " ", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + ",", + " ", + "t", + "y", + "p", + "e", + "(", + "r", + ")", + " ", + "A", + "S", + " ", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + ",", + " ", + "b", + ".", + "n", + "a", + "m", + "e", + " ", + "A", + "S", + " ", + "o", + "b", + "j", + "e", + "c", + "t", + "\"", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "p", + "a", + "r", + "a", + "m", + "s", + "=", + "{", + "\"", + "s", + "e", + "s", + "s", + "\"", + ":", + " ", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "}", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + " ", + " ", + " ", + " ", + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "r", + "o", + "w", + "s", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "N", + "o", + " ", + "f", + "a", + "c", + "t", + "s", + " ", + "s", + "t", + "o", + "r", + "e", + "d", + " ", + "y", + "e", + "t", + "\"", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "\"", + "\\", + "n", + "\"", + ".", + "j", + "o", + "i", + "n", + "(", + "f", + "\"", + "{", + "r", + "[", + "'", + "s", + "u", + "b", + "j", + "e", + "c", + "t", + "'", + "]", + "}", + " ", + "-", + "[", + "{", + "r", + "[", + "'", + "r", + "e", + "l", + "a", + "t", + "i", + "o", + "n", + "'", + "]", + "}", + "]", + "-", + ">", + " ", + "{", + "r", + "[", + "'", + "o", + "b", + "j", + "e", + "c", + "t", + "'", + "]", + "}", + "\"", + " ", + "f", + "o", + "r", + " ", + "r", + " ", + "i", + "n", + " ", + "r", + "o", + "w", + "s", + ")", + "\n", + "\n", + "\n", + "t", + "o", + "o", + "l", + "s", + " ", + "=", + " ", + "[", + "s", + "a", + "v", + "e", + "_", + "f", + "a", + "c", + "t", + ",", + " ", + "q", + "u", + "e", + "r", + "y", + "_", + "f", + "a", + "c", + "t", + "s", + ",", + " ", + "f", + "i", + "n", + "d", + "_", + "r", + "e", + "l", + "a", + "t", + "e", + "d", + ",", + " ", + "l", + "i", + "s", + "t", + "_", + "a", + "l", + "l", + "_", + "f", + "a", + "c", + "t", + "s", + "]", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "S", + "e", + "s", + "s", + "i", + "o", + "n", + ":", + " ", + "{", + "S", + "E", + "S", + "S", + "I", + "O", + "N", + "}", + "\"", + ")", + "\n", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "T", + "o", + "o", + "l", + "s", + ":", + "\"", + ",", + " ", + "[", + "t", + ".", + "n", + "a", + "m", + "e", + " ", + "f", + "o", + "r", + " ", + "t", + " ", + "i", + "n", + " ", + "t", + "o", + "o", + "l", + "s", + "]", + ")" ] }, { @@ -418,4 +5882,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From 3ee3f23b971c353cccbf0663868d7612ab2d0ca3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 10:32:33 +0300 Subject: [PATCH 27/86] build(deps): bump coordinode-rs to v0.3.10, pin docker image to 0.3.10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coordinode-rs submodule: v0.3.9 → v0.3.10 (follower reads, replication proto, parallel memtable writes, CI fixes) - docker-compose.yml: latest → 0.3.10 (pin to tested server version) - All 119 integration tests pass against 0.3.10 - FTS (TextService) remains UNIMPLEMENTED in 0.3.10 — xfail markers kept --- coordinode-rs | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index f6558cc..38a52cf 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit f6558ccf19322cd86815ef303bd2bd337bd9fc42 +Subproject commit 38a52cf505c9a8130ae5845e66b211ae10fffe7f diff --git a/docker-compose.yml b/docker-compose.yml index 5cf2f39..2c01e75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ version: "3.9" services: coordinode: - image: ghcr.io/structured-world/coordinode:latest + image: ghcr.io/structured-world/coordinode:0.3.10 container_name: coordinode ports: - "7080:7080" # gRPC From ccd9a24a465ba917f20cb6f54c344611aac968fc Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 10:39:54 +0300 Subject: [PATCH 28/86] fix(notebooks): guard LocalClient import, fix tagged QA query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Wrap `from coordinode_embedded import LocalClient` in try/except ImportError in all four demo notebooks — raises RuntimeError with install instructions instead of a bare ModuleNotFoundError when coordinode-embedded is not installed locally - Fix GraphCypherQAChain demo query: `chain.invoke(f"Who influenced Shannon-{tag}?")` — nodes are created with tag suffix so the natural-language question must reference the tagged name --- demo/notebooks/00_seed_data.ipynb | 36 +- .../01_llama_index_property_graph.ipynb | 38 +- demo/notebooks/02_langchain_graph_chain.ipynb | 597 +----------------- demo/notebooks/03_langgraph_agent.ipynb | 36 +- 4 files changed, 9 insertions(+), 698 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 2d37acc..9426253 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -3059,39 +3059,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": [ - "import os, socket\n", - "\n", - "\n", - "def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", - "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "else:\n", - " # No server available — use the embedded in-process engine.\n", - " from coordinode_embedded import LocalClient\n", - "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" - ] + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", @@ -9301,4 +9269,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 298d321..762d9c3 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -2071,41 +2071,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": [ - "import os, socket\n", - "\n", - "\n", - "def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", - "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "else:\n", - " # No server available — use the embedded in-process engine.\n", - " # Works without Docker or any external service; data is in-memory.\n", - " from coordinode_embedded import LocalClient\n", - "\n", - " _lc = LocalClient(\":memory:\")\n", - " client = _EmbeddedAdapter(_lc)\n", - " print(\"Using embedded LocalClient (in-process)\")" - ] + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", @@ -2314,4 +2280,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index b4f70d0..c19cbfb 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -2038,40 +2038,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": [ - "import os, socket\n", - "\n", - "\n", - "def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", - "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "else:\n", - " # No server available — use the embedded in-process engine.\n", - " from coordinode_embedded import LocalClient\n", - "\n", - " _lc = LocalClient(\":memory:\")\n", - " client = _EmbeddedAdapter(_lc)\n", - " print(\"Using embedded LocalClient (in-process)\")" - ] + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", @@ -2225,565 +2192,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000019", "metadata": {}, "outputs": [], - "source": [ - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - "\"", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "'", - "S", - "k", - "i", - "p", - "p", - "i", - "n", - "g", - ":", - " ", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "s", - "e", - "t", - ".", - " ", - "S", - "e", - "t", - " ", - "i", - "t", - " ", - "v", - "i", - "a", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - "\"", - "]", - " ", - "=", - " ", - "\"", - "s", - "k", - "-", - ".", - ".", - ".", - "\"", - " ", - "a", - "n", - "d", - " ", - "r", - "e", - "-", - "r", - "u", - "n", - " ", - "t", - "h", - "i", - "s", - " ", - "c", - "e", - "l", - "l", - ".", - "'", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "e", - "l", - "s", - "e", - ":", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - ".", - "c", - "h", - "a", - "i", - "n", - "s", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "G", - "r", - "a", - "p", - "h", - "C", - "y", - "p", - "h", - "e", - "r", - "Q", - "A", - "C", - "h", - "a", - "i", - "n", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "_", - "o", - "p", - "e", - "n", - "a", - "i", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "h", - "a", - "t", - "O", - "p", - "e", - "n", - "A", - "I", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "a", - "i", - "n", - " ", - "=", - " ", - "G", - "r", - "a", - "p", - "h", - "C", - "y", - "p", - "h", - "e", - "r", - "Q", - "A", - "C", - "h", - "a", - "i", - "n", - ".", - "f", - "r", - "o", - "m", - "_", - "l", - "l", - "m", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "C", - "h", - "a", - "t", - "O", - "p", - "e", - "n", - "A", - "I", - "(", - "m", - "o", - "d", - "e", - "l", - "=", - "\"", - "g", - "p", - "t", - "-", - "4", - "o", - "-", - "m", - "i", - "n", - "i", - "\"", - ",", - " ", - "t", - "e", - "m", - "p", - "e", - "r", - "a", - "t", - "u", - "r", - "e", - "=", - "0", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "g", - "r", - "a", - "p", - "h", - "=", - "g", - "r", - "a", - "p", - "h", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "v", - "e", - "r", - "b", - "o", - "s", - "e", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "a", - "l", - "l", - "o", - "w", - "_", - "d", - "a", - "n", - "g", - "e", - "r", - "o", - "u", - "s", - "_", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - "s", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "u", - "l", - "t", - " ", - "=", - " ", - "c", - "h", - "a", - "i", - "n", - ".", - "i", - "n", - "v", - "o", - "k", - "e", - "(", - "\"", - "W", - "h", - "o", - " ", - "i", - "n", - "f", - "l", - "u", - "e", - "n", - "c", - "e", - "d", - " ", - "S", - "h", - "a", - "n", - "n", - "o", - "n", - "?", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "A", - "n", - "s", - "w", - "e", - "r", - ":", - "\"", - ",", - " ", - "r", - "e", - "s", - "u", - "l", - "t", - "[", - "\"", - "r", - "e", - "s", - "u", - "l", - "t", - "\"", - "]", - ")" - ] + "source": "if not os.environ.get(\"OPENAI_API_KEY\"):\n print(\n 'Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.'\n )\nelse:\n from langchain.chains import GraphCypherQAChain\n from langchain_openai import ChatOpenAI\n\n chain = GraphCypherQAChain.from_llm(\n ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n graph=graph,\n verbose=True,\n allow_dangerous_requests=True,\n )\n result = chain.invoke(f\"Who influenced Shannon-{tag}?\")\n print(\"Answer:\", result[\"result\"])" }, { "cell_type": "markdown", @@ -2819,4 +2228,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index c0a29cf..82d0e61 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -1988,39 +1988,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": [ - "import os, socket\n", - "\n", - "\n", - "def _port_open(port):\n", - " try:\n", - " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", - " return True\n", - " except OSError:\n", - " return False\n", - "\n", - "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", - "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "else:\n", - " # No server available — use the embedded in-process engine.\n", - " from coordinode_embedded import LocalClient\n", - "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" - ] + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", @@ -5882,4 +5850,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file From be9d639295a42519742c2d26b2b5507b39957b17 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 10:43:59 +0300 Subject: [PATCH 29/86] =?UTF-8?q?fix(graph):=20remove=20\s*=20from=20=5Fpa?= =?UTF-8?q?rse=5Fschema=20regex=20to=20eliminate=20O(n=C2=B2)=20backtracki?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `\s*([^)\n]+)` had quadratic backtracking: spaces match both \s* and [^)\n], so the engine re-partitions them on every mismatch. Drop \s* — the character class already captures leading whitespace, and prop_str.strip() handles it. --- langchain-coordinode/langchain_coordinode/graph.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 6fe6870..d43f268 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -451,9 +451,11 @@ def _parse_schema(schema_text: str) -> dict[str, Any]: continue # Parse inline properties: "- Label (properties: prop1: TYPE, prop2: TYPE)" props: list[dict[str, str]] = [] - # [^)\n]+ matches property list characters without crossing line or ')'. - # Each character in the class is a simple literal — no backtracking risk. - m = re.search(r"\(properties:\s*([^)\n]+)\)", stripped) + # [^)\n]+ has no overlap with the surrounding literal chars, so backtracking + # is bounded at O(n). Drop the earlier \s* — its overlap with [^)\n] + # (spaces match both) created O(n²) backtracking on malformed input. + # Leading/trailing whitespace is handled by prop_str.strip() below. + m = re.search(r"\(properties:([^)\n]+)\)", stripped) if m: for prop_str in m.group(1).split(","): kv = prop_str.strip().split(":", 1) From fb58282de1ce36766d2a265bc8af9f2f016ee6bf Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 11:02:58 +0300 Subject: [PATCH 30/86] fix(demo): restore notebook source format and add network timeouts All four demo notebooks had install cells stored as char-by-char JSON arrays, making cells non-executable in Jupyter. Notebooks 00 and 03 had additional broken cells. - Convert char-by-char source arrays to proper line-per-string format - Add timeout=30 to urlopen (rustup download) - Add timeout=300/300/600 to subprocess.run (rustup, maturin, embedded pip) - Add timeout=300 to unconditional pip install --- demo/notebooks/00_seed_data.ipynb | 8302 +---------------- .../01_llama_index_property_graph.ipynb | 2018 +--- demo/notebooks/02_langchain_graph_chain.ipynb | 1989 +--- demo/notebooks/03_langgraph_agent.ipynb | 5731 +----------- 4 files changed, 519 insertions(+), 17521 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 9426253..ff42b99 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -1146,8050 +1146,270 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "y", - "s", - ",", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - "\n", - "\n", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - " ", - "=", - " ", - "\"", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "l", - "a", - "b", - "\"", - " ", - "i", - "n", - " ", - "s", - "y", - "s", - ".", - "m", - "o", - "d", - "u", - "l", - "e", - "s", - "\n", - "\n", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "o", - "n", - "l", - "y", - " ", - "w", - "h", - "e", - "n", - " ", - "n", - "o", - " ", - "g", - "R", - "P", - "C", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "i", - "s", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "(", - "C", - "o", - "l", - "a", - "b", - " ", - "p", - "a", - "t", - "h", - ")", - ".", - "\n", - "i", - "f", - " ", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "R", - "u", - "s", - "t", - " ", - "t", - "o", - "o", - "l", - "c", - "h", - "a", - "i", - "n", - " ", - "v", - "i", - "a", - " ", - "r", - "u", - "s", - "t", - "u", - "p", - " ", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "C", - "o", - "l", - "a", - "b", - "'", - "s", - " ", - "a", - "p", - "t", - " ", - "p", - "a", - "c", - "k", - "a", - "g", - "e", - "s", - " ", - "s", - "h", - "i", - "p", - " ", - "r", - "u", - "s", - "t", - "c", - " ", - "≤", - "1", - ".", - "7", - "5", - ",", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "c", - "a", - "n", - "n", - "o", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "≥", - "1", - ".", - "8", - "0", - " ", - "f", - "o", - "r", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "y", - "o", - "3", - ")", - ".", - " ", - "a", - "p", - "t", - "-", - "g", - "e", - "t", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "a", - " ", - "v", - "i", - "a", - "b", - "l", - "e", - " ", - "a", - "l", - "t", - "e", - "r", - "n", - "a", - "t", - "i", - "v", - "e", - " ", - "h", - "e", - "r", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "D", - "o", - "w", - "n", - "l", - "o", - "a", - "d", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "r", - " ", - "t", - "o", - " ", - "a", - " ", - "t", - "e", - "m", - "p", - " ", - "f", - "i", - "l", - "e", - " ", - "a", - "n", - "d", - " ", - "e", - "x", - "e", - "c", - "u", - "t", - "e", - " ", - "i", - "t", - " ", - "e", - "x", - "p", - "l", - "i", - "c", - "i", - "t", - "l", - "y", - " ", - "—", - " ", - "t", - "h", - "i", - "s", - " ", - "a", - "v", - "o", - "i", - "d", - "s", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "p", - "i", - "p", - "i", - "n", - "g", - " ", - "r", - "e", - "m", - "o", - "t", - "e", - " ", - "c", - "o", - "n", - "t", - "e", - "n", - "t", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "l", - "y", - " ", - "i", - "n", - "t", - "o", - " ", - "a", - " ", - "s", - "h", - "e", - "l", - "l", - " ", - "w", - "h", - "i", - "l", - "e", - " ", - "m", - "a", - "i", - "n", - "t", - "a", - "i", - "n", - "i", - "n", - "g", - " ", - "H", - "T", - "T", - "P", - "S", - "/", - "T", - "L", - "S", - " ", - "s", - "e", - "c", - "u", - "r", - "i", - "t", - "y", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "t", - "h", - "r", - "o", - "u", - "g", - "h", - " ", - "P", - "y", - "t", - "h", - "o", - "n", - "'", - "s", - " ", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - " ", - "s", - "s", - "l", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - " ", - "(", - "c", - "e", - "r", - "t", - "-", - "v", - "e", - "r", - "i", - "f", - "i", - "e", - "d", - ",", - " ", - "T", - "L", - "S", - " ", - "1", - ".", - "2", - "+", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "s", - "l", - " ", - "a", - "s", - " ", - "_", - "s", - "s", - "l", - ",", - " ", - "t", - "e", - "m", - "p", - "f", - "i", - "l", - "e", - " ", - "a", - "s", - " ", - "_", - "t", - "m", - "p", - ",", - " ", - "u", - "r", - "l", - "l", - "i", - "b", - ".", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - " ", - "a", - "s", - " ", - "_", - "u", - "r", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "t", - "x", - " ", - "=", - " ", - "_", - "s", - "s", - "l", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - "_", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "t", - "m", - "p", - ".", - "N", - "a", - "m", - "e", - "d", - "T", - "e", - "m", - "p", - "o", - "r", - "a", - "r", - "y", - "F", - "i", - "l", - "e", - "(", - "m", - "o", - "d", - "e", - "=", - "\"", - "w", - "b", - "\"", - ",", - " ", - "s", - "u", - "f", - "f", - "i", - "x", - "=", - "\"", - ".", - "s", - "h", - "\"", - ",", - " ", - "d", - "e", - "l", - "e", - "t", - "e", - "=", - "F", - "a", - "l", - "s", - "e", - ")", - " ", - "a", - "s", - " ", - "_", - "f", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "u", - "r", - ".", - "u", - "r", - "l", - "o", - "p", - "e", - "n", - "(", - "\"", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "s", - "h", - ".", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - "\"", - ",", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "=", - "_", - "c", - "t", - "x", - ")", - " ", - "a", - "s", - " ", - "_", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "f", - ".", - "w", - "r", - "i", - "t", - "e", - "(", - "_", - "r", - ".", - "r", - "e", - "a", - "d", - "(", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - " ", - "=", - " ", - "_", - "f", - ".", - "n", - "a", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "\"", - "/", - "b", - "i", - "n", - "/", - "s", - "h", - "\"", - ",", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ",", - " ", - "\"", - "-", - "s", - "\"", - ",", - " ", - "\"", - "-", - "-", - "\"", - ",", - " ", - "\"", - "-", - "y", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "f", - "i", - "n", - "a", - "l", - "l", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "u", - "n", - "l", - "i", - "n", - "k", - "(", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "d", - "d", - " ", - "c", - "a", - "r", - "g", - "o", - " ", - "t", - "o", - " ", - "P", - "A", - "T", - "H", - " ", - "s", - "o", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "i", - "p", - " ", - "c", - "a", - "n", - " ", - "f", - "i", - "n", - "d", - " ", - "i", - "t", - ".", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - " ", - "=", - " ", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - ".", - "e", - "x", - "p", - "a", - "n", - "d", - "u", - "s", - "e", - "r", - "(", - "\"", - "~", - "/", - ".", - "c", - "a", - "r", - "g", - "o", - "/", - "b", - "i", - "n", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "P", - "A", - "T", - "H", - "\"", - "]", - " ", - "=", - " ", - "f", - "\"", - "{", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - "}", - "{", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - "s", - "e", - "p", - "}", - "{", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "'", - "P", - "A", - "T", - "H", - "'", - ",", - " ", - "'", - "'", - ")", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - " ", - "\"", - "-", - "m", - "\"", - ",", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - ",", - " ", - "\"", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - ")", - "\n", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - "\n", - "\n", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - ".", - "a", - "p", - "p", - "l", - "y", - "(", - ")", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "R", - "e", - "a", - "d", - "y", - "\"", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000004", - "metadata": {}, - "source": [ - "## Connect to CoordiNode\n", - "\n", - "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", - "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", - "- **Local without server**: falls back to embedded engine automatically." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000005", - "metadata": {}, - "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000006", - "metadata": {}, - "source": [ - "## Step 1 — Clear previous demo data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000007", - "metadata": {}, - "outputs": [], - "source": [ - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - " ", - "=", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - "\"", - ",", - " ", - "\"", - "s", - "e", - "e", - "d", - "_", - "d", - "a", - "t", - "a", - "\"", - ")", - "\n", - "r", - "e", - "s", - "u", - "l", - "t", - " ", - "=", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - " ", - "{", - "d", - "e", - "m", - "o", - ":", - " ", - "t", - "r", - "u", - "e", - ",", - " ", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - ":", - " ", - "$", - "t", - "a", - "g", - "}", - ")", - " ", - "D", - "E", - "T", - "A", - "C", - "H", - " ", - "D", - "E", - "L", - "E", - "T", - "E", - " ", - "n", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "t", - "a", - "g", - "\"", - ":", - " ", - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - "}", - ",", - "\n", - ")", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "P", - "r", - "e", - "v", - "i", - "o", - "u", - "s", - " ", - "d", - "e", - "m", - "o", - " ", - "d", - "a", - "t", - "a", - " ", - "r", - "e", - "m", - "o", - "v", - "e", - "d", - "\"", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000008", - "metadata": {}, - "source": [ - "## Step 2 — Create nodes" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000009", - "metadata": {}, - "outputs": [], - "source": [ - "#", - " ", - "─", - "─", - " ", - "P", - "e", - "o", - "p", - "l", - "e", - " ", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "\n", - "p", - "e", - "o", - "p", - "l", - "e", - " ", - "=", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "M", - "L", - " ", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "e", - "r", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "R", - "e", - "i", - "n", - "f", - "o", - "r", - "c", - "e", - "m", - "e", - "n", - "t", - " ", - "L", - "e", - "a", - "r", - "n", - "i", - "n", - "g", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "B", - "o", - "b", - " ", - "T", - "o", - "r", - "r", - "e", - "s", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "S", - "t", - "a", - "f", - "f", - " ", - "E", - "n", - "g", - "i", - "n", - "e", - "e", - "r", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "D", - "i", - "s", - "t", - "r", - "i", - "b", - "u", - "t", - "e", - "d", - " ", - "S", - "y", - "s", - "t", - "e", - "m", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "F", - "o", - "u", - "n", - "d", - "e", - "r", - " ", - "&", - " ", - "C", - "E", - "O", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "N", - "L", - "P", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - " ", - "S", - "c", - "i", - "e", - "n", - "t", - "i", - "s", - "t", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "L", - "L", - "M", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "E", - "v", - "a", - " ", - "M", - "ü", - "l", - "l", - "e", - "r", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "S", - "y", - "s", - "t", - "e", - "m", - "s", - " ", - "A", - "r", - "c", - "h", - "i", - "t", - "e", - "c", - "t", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "P", - "r", - "i", - "n", - "c", - "i", - "p", - "a", - "l", - " ", - "E", - "n", - "g", - "i", - "n", - "e", - "e", - "r", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "M", - "e", - "t", - "a", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "M", - "L", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "P", - "h", - "D", - " ", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "e", - "r", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "M", - "I", - "T", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "H", - "e", - "n", - "r", - "y", - " ", - "R", - "o", - "s", - "s", - "i", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "C", - "T", - "O", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "S", - "e", - "n", - "i", - "o", - "r", - " ", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "e", - "r", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "s", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "J", - "a", - "m", - "e", - "s", - " ", - "W", - "r", - "i", - "g", - "h", - "t", - "\"", - ",", - " ", - "\"", - "r", - "o", - "l", - "e", - "\"", - ":", - " ", - "\"", - "E", - "n", - "g", - "i", - "n", - "e", - "e", - "r", - "i", - "n", - "g", - " ", - "L", - "e", - "a", - "d", - "\"", - ",", - " ", - "\"", - "o", - "r", - "g", - "\"", - ":", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "f", - "i", - "e", - "l", - "d", - "\"", - ":", - " ", - "\"", - "S", - "e", - "a", - "r", - "c", - "h", - "\"", - "}", - ",", - "\n", - "]", - "\n", - "\n", - "f", - "o", - "r", - " ", - "p", - " ", - "i", - "n", - " ", - "p", - "e", - "o", - "p", - "l", - "e", - ":", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "n", - ":", - "P", - "e", - "r", - "s", - "o", - "n", - " ", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "n", - "a", - "m", - "e", - "}", - ")", - " ", - "S", - "E", - "T", - " ", - "n", - ".", - "r", - "o", - "l", - "e", - " ", - "=", - " ", - "$", - "r", - "o", - "l", - "e", - ",", - " ", - "n", - ".", - "f", - "i", - "e", - "l", - "d", - " ", - "=", - " ", - "$", - "f", - "i", - "e", - "l", - "d", - ",", - " ", - "n", - ".", - "d", - "e", - "m", - "o", - " ", - "=", - " ", - "t", - "r", - "u", - "e", - "\"", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "p", - ")", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "r", - "e", - "a", - "t", - "e", - "d", - " ", - "{", - "l", - "e", - "n", - "(", - "p", - "e", - "o", - "p", - "l", - "e", - ")", - "}", - " ", - "p", - "e", - "o", - "p", - "l", - "e", - "\"", - ")", - "\n", - "\n", - "#", - " ", - "─", - "─", - " ", - "C", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - " ", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "\n", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - " ", - "=", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "y", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "1", - "9", - "9", - "8", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "M", - "o", - "u", - "n", - "t", - "a", - "i", - "n", - " ", - "V", - "i", - "e", - "w", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "M", - "e", - "t", - "a", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "y", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "2", - "0", - "0", - "4", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "M", - "e", - "n", - "l", - "o", - " ", - "P", - "a", - "r", - "k", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "A", - "I", - " ", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "2", - "0", - "1", - "5", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "S", - "a", - "n", - " ", - "F", - "r", - "a", - "n", - "c", - "i", - "s", - "c", - "o", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "A", - "I", - " ", - "R", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "2", - "0", - "1", - "0", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "L", - "o", - "n", - "d", - "o", - "n", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "A", - "I", - " ", - "S", - "t", - "a", - "r", - "t", - "u", - "p", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "B", - "e", - "r", - "l", - "i", - "n", - "\"", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "M", - "I", - "T", - "\"", - ",", - " ", - "\"", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - "\"", - ":", - " ", - "\"", - "A", - "c", - "a", - "d", - "e", - "m", - "i", - "a", - "\"", - ",", - " ", - "\"", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - "\"", - ":", - " ", - "1", - "8", - "6", - "1", - ",", - " ", - "\"", - "h", - "q", - "\"", - ":", - " ", - "\"", - "C", - "a", - "m", - "b", - "r", - "i", - "d", - "g", - "e", - "\"", - "}", - ",", - "\n", - "]", - "\n", - "\n", - "f", - "o", - "r", - " ", - "c", - " ", - "i", - "n", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "n", - ":", - "C", - "o", - "m", - "p", - "a", - "n", - "y", - " ", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "n", - "a", - "m", - "e", - ",", - " ", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - ":", - " ", - "$", - "t", - "a", - "g", - "}", - ")", - " ", - "S", - "E", - "T", - " ", - "n", - ".", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - " ", - "=", - " ", - "$", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - ",", - " ", - "n", - ".", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - " ", - "=", - " ", - "$", - "f", - "o", - "u", - "n", - "d", - "e", - "d", - ",", - " ", - "n", - ".", - "h", - "q", - " ", - "=", - " ", - "$", - "h", - "q", - ",", - " ", - "n", - ".", - "d", - "e", - "m", - "o", - " ", - "=", - " ", - "t", - "r", - "u", - "e", - ",", - " ", - "n", - ".", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - " ", - "=", - " ", - "$", - "t", - "a", - "g", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "*", - "*", - "c", - ",", - " ", - "\"", - "t", - "a", - "g", - "\"", - ":", - " ", - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "r", - "e", - "a", - "t", - "e", - "d", - " ", - "{", - "l", - "e", - "n", - "(", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - ")", - "}", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - "\"", - ")", - "\n", - "\n", - "#", - " ", - "─", - "─", - " ", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - " ", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "─", - "\n", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - " ", - "=", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "A", - "r", - "c", - "h", - "i", - "t", - "e", - "c", - "t", - "u", - "r", - "e", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "7", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "A", - "l", - "g", - "o", - "r", - "i", - "t", - "h", - "m", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "0", - "9", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "R", - "e", - "i", - "n", - "f", - "o", - "r", - "c", - "e", - "m", - "e", - "n", - "t", - " ", - "L", - "e", - "a", - "r", - "n", - "i", - "n", - "g", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "P", - "a", - "r", - "a", - "d", - "i", - "g", - "m", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "1", - "9", - "8", - "0", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "D", - "a", - "t", - "a", - " ", - "M", - "o", - "d", - "e", - "l", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "2", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "I", - "n", - "f", - "r", - "a", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "9", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "i", - "q", - "u", - "e", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "0", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "L", - "L", - "M", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "M", - "o", - "d", - "e", - "l", - " ", - "C", - "l", - "a", - "s", - "s", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "8", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "t", - "y", - "p", - "e", - "\"", - ":", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "i", - "q", - "u", - "e", - "\"", - ",", - " ", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "3", - "}", - ",", - "\n", - "]", - "\n", - "\n", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "n", - ":", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "y", - " ", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "n", - "a", - "m", - "e", - ",", - " ", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - ":", - " ", - "$", - "t", - "a", - "g", - "}", - ")", - " ", - "S", - "E", - "T", - " ", - "n", - ".", - "t", - "y", - "p", - "e", - " ", - "=", - " ", - "$", - "t", - "y", - "p", - "e", - ",", - " ", - "n", - ".", - "y", - "e", - "a", - "r", - " ", - "=", - " ", - "$", - "y", - "e", - "a", - "r", - ",", - " ", - "n", - ".", - "d", - "e", - "m", - "o", - " ", - "=", - " ", - "t", - "r", - "u", - "e", - ",", - " ", - "n", - ".", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - " ", - "=", - " ", - "$", - "t", - "a", - "g", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "*", - "*", - "t", - ",", - " ", - "\"", - "t", - "a", - "g", - "\"", - ":", - " ", - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "r", - "e", - "a", - "t", - "e", - "d", - " ", - "{", - "l", - "e", - "n", - "(", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - ")", - "}", - " ", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - "\"", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000010", - "metadata": {}, - "source": [ - "## Step 3 — Create relationships" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000011", - "metadata": {}, - "outputs": [], - "source": [ - "e", - "d", - "g", - "e", - "s", - " ", - "=", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "B", - "o", - "b", - " ", - "T", - "o", - "r", - "r", - "e", - "s", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "E", - "v", - "a", - " ", - "M", - "ü", - "l", - "l", - "e", - "r", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "2", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "M", - "e", - "t", - "a", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "M", - "I", - "T", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "H", - "e", - "n", - "r", - "y", - " ", - "R", - "o", - "s", - "s", - "i", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "J", - "a", - "m", - "e", - "s", - " ", - "W", - "r", - "i", - "g", - "h", - "t", - "\"", - ",", - " ", - "\"", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "\"", - ",", - " ", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "H", - "e", - "n", - "r", - "y", - " ", - "R", - "o", - "s", - "s", - "i", - "\"", - ",", - " ", - "\"", - "C", - "O", - "_", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - "\"", - ",", - " ", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "K", - "N", - "O", - "W", - "S", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "C", - "a", - "r", - "o", - "l", - " ", - "S", - "m", - "i", - "t", - "h", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "B", - "o", - "b", - " ", - "T", - "o", - "r", - "r", - "e", - "s", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "J", - "a", - "m", - "e", - "s", - " ", - "W", - "r", - "i", - "g", - "h", - "t", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "E", - "v", - "a", - " ", - "M", - "ü", - "l", - "l", - "e", - "r", - "\"", - ",", - " ", - "\"", - "K", - "N", - "O", - "W", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - " ", - "/", - " ", - "W", - "O", - "R", - "K", - "S", - "_", - "O", - "N", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "A", - "l", - "i", - "c", - "e", - " ", - "C", - "h", - "e", - "n", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "R", - "e", - "i", - "n", - "f", - "o", - "r", - "c", - "e", - "m", - "e", - "n", - "t", - " ", - "L", - "e", - "a", - "r", - "n", - "i", - "n", - "g", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "1", - "9", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "D", - "a", - "v", - "i", - "d", - " ", - "P", - "a", - "r", - "k", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "L", - "L", - "M", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "0", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "1", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "I", - "s", - "l", - "a", - " ", - "N", - "a", - "k", - "a", - "m", - "u", - "r", - "a", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "0", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "F", - "r", - "a", - "n", - "k", - " ", - "L", - "i", - "u", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - " ", - "N", - "e", - "u", - "r", - "a", - "l", - " ", - "N", - "e", - "t", - "w", - "o", - "r", - "k", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "c", - "e", - " ", - "O", - "k", - "a", - "f", - "o", - "r", - "\"", - ",", - " ", - "\"", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - "\"", - ",", - " ", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "\"", - "s", - "i", - "n", - "c", - "e", - "\"", - ":", - " ", - "2", - "0", - "2", - "3", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "U", - "S", - "E", - "S", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "S", - "y", - "n", - "t", - "h", - "e", - "x", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "O", - "p", - "e", - "n", - "A", - "I", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "U", - "S", - "E", - "S", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "C", - "Q", - "U", - "I", - "R", - "E", - "D", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - ",", - " ", - "\"", - "A", - "C", - "Q", - "U", - "I", - "R", - "E", - "D", - "\"", - ",", - " ", - "\"", - "D", - "e", - "e", - "p", - "M", - "i", - "n", - "d", - "\"", - ",", - " ", - "{", - "\"", - "y", - "e", - "a", - "r", - "\"", - ":", - " ", - "2", - "0", - "1", - "4", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "K", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "G", - "r", - "a", - "p", - "h", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "G", - "r", - "a", - "p", - "h", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "R", - "A", - "G", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "V", - "e", - "c", - "t", - "o", - "r", - " ", - "D", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - "(", - "\"", - "L", - "L", - "M", - "\"", - ",", - " ", - "\"", - "B", - "U", - "I", - "L", - "D", - "S", - "_", - "O", - "N", - "\"", - ",", - " ", - "\"", - "T", - "r", - "a", - "n", - "s", - "f", - "o", - "r", - "m", - "e", - "r", - "\"", - ",", - " ", - "{", - "}", - ")", - ",", - "\n", - "]", - "\n", - "\n", - "s", - "r", - "c", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "p", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "p", - " ", - "i", - "n", - " ", - "p", - "e", - "o", - "p", - "l", - "e", - "}", - "\n", - "t", - "e", - "c", - "h", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "t", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - "}", - "\n", - "c", - "o", - "m", - "p", - "a", - "n", - "y", - "_", - "n", - "a", - "m", - "e", - "s", - " ", - "=", - " ", - "{", - "c", - "[", - "\"", - "n", - "a", - "m", - "e", - "\"", - "]", - " ", - "f", - "o", - "r", - " ", - "c", - " ", - "i", - "n", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - "}", - "\n", - "\n", - "\n", - "d", - "e", - "f", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "n", - "a", - "m", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "s", - "r", - "c", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "P", - "e", - "r", - "s", - "o", - "n", - "\"", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "t", - "e", - "c", - "h", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "T", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "y", - "\"", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "a", - "m", - "e", - " ", - "i", - "n", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "y", - "_", - "n", - "a", - "m", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "C", - "o", - "m", - "p", - "a", - "n", - "y", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "a", - "i", - "s", - "e", - " ", - "V", - "a", - "l", - "u", - "e", - "E", - "r", - "r", - "o", - "r", - "(", - "f", - "\"", - "U", - "n", - "k", - "n", - "o", - "w", - "n", - " ", - "e", - "d", - "g", - "e", - " ", - "e", - "n", - "d", - "p", - "o", - "i", - "n", - "t", - ":", - " ", - "{", - "n", - "a", - "m", - "e", - "!", - "r", - "}", - "\"", - ")", - "\n", - "\n", - "\n", - "f", - "o", - "r", - " ", - "s", - "r", - "c", - ",", - " ", - "r", - "e", - "l", - ",", - " ", - "d", - "s", - "t", - ",", - " ", - "p", - "r", - "o", - "p", - "s", - " ", - "i", - "n", - " ", - "e", - "d", - "g", - "e", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - "s", - "r", - "c", - "_", - "l", - "a", - "b", - "e", - "l", - " ", - "=", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "s", - "r", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - "d", - "s", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - " ", - "=", - " ", - "_", - "l", - "a", - "b", - "e", - "l", - "(", - "d", - "s", - "t", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - " ", - "=", - " ", - "\"", - ",", - " ", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "f", - "\"", - "r", - ".", - "{", - "k", - "}", - " ", - "=", - " ", - "$", - "{", - "k", - "}", - "\"", - " ", - "f", - "o", - "r", - " ", - "k", - " ", - "i", - "n", - " ", - "p", - "r", - "o", - "p", - "s", - ")", - " ", - "i", - "f", - " ", - "p", - "r", - "o", - "p", - "s", - " ", - "e", - "l", - "s", - "e", - " ", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "e", - "t", - "_", - "p", - "a", - "r", - "t", - " ", - "=", - " ", - "f", - "\"", - " ", - "S", - "E", - "T", - " ", - "{", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - "}", - "\"", - " ", - "i", - "f", - " ", - "s", - "e", - "t", - "_", - "c", - "l", - "a", - "u", - "s", - "e", - " ", - "e", - "l", - "s", - "e", - " ", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "a", - ":", - "{", - "s", - "r", - "c", - "_", - "l", - "a", - "b", - "e", - "l", - "}", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "s", - "r", - "c", - ",", - " ", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - ":", - " ", - "$", - "t", - "a", - "g", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "b", - ":", - "{", - "d", - "s", - "t", - "_", - "l", - "a", - "b", - "e", - "l", - "}", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "d", - "s", - "t", - ",", - " ", - "d", - "e", - "m", - "o", - "_", - "t", - "a", - "g", - ":", - " ", - "$", - "t", - "a", - "g", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "a", - ")", - "-", - "[", - "r", - ":", - "{", - "r", - "e", - "l", - "}", - "]", - "-", - ">", - "(", - "b", - ")", - "\"", - " ", - "+", - " ", - "s", - "e", - "t", - "_", - "p", - "a", - "r", - "t", - ",", + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded only when no gRPC server is available (Colab path).\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "s", - "r", - "c", - "\"", - ":", - " ", - "s", - "r", - "c", - ",", - " ", - "\"", - "d", - "s", - "t", - "\"", - ":", - " ", - "d", - "s", - "t", - ",", - " ", - "\"", - "t", - "a", - "g", - "\"", - ":", - " ", - "D", - "E", - "M", - "O", - "_", - "T", - "A", - "G", - ",", - " ", - "*", - "*", - "p", - "r", - "o", - "p", - "s", - "}", - ",", + "import nest_asyncio\n", "\n", - " ", - " ", - " ", - " ", - ")", + "nest_asyncio.apply()\n", "\n", + "print(\"Ready\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000004", + "metadata": {}, + "source": [ + "## Connect to CoordiNode\n", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "r", - "e", - "a", - "t", - "e", - "d", - " ", - "{", - "l", - "e", - "n", - "(", - "e", - "d", - "g", - "e", - "s", - ")", - "}", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - "s", - "h", - "i", - "p", - "s", - "\"", - ")" + "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", + "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", + "- **Local without server**: falls back to embedded engine automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000005", + "metadata": {}, + "outputs": [], + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000006", + "metadata": {}, + "source": [ + "## Step 1 — Clear previous demo data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000007", + "metadata": {}, + "outputs": [], + "source": [ + "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\", \"seed_data\")\n", + "result = client.cypher(\n", + " \"MATCH (n {demo: true, demo_tag: $tag}) DETACH DELETE n\",\n", + " params={\"tag\": DEMO_TAG},\n", + ")\n", + "print(\"Previous demo data removed\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000008", + "metadata": {}, + "source": [ + "## Step 2 — Create nodes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000009", + "metadata": {}, + "outputs": [], + "source": [ + "# ── People ────────────────────────────────────────────────────────────────\n", + "people = [\n", + " {\"name\": \"Alice Chen\", \"role\": \"ML Researcher\", \"org\": \"DeepMind\", \"field\": \"Reinforcement Learning\"},\n", + " {\"name\": \"Bob Torres\", \"role\": \"Staff Engineer\", \"org\": \"Google\", \"field\": \"Distributed Systems\"},\n", + " {\"name\": \"Carol Smith\", \"role\": \"Founder & CEO\", \"org\": \"Synthex\", \"field\": \"NLP\"},\n", + " {\"name\": \"David Park\", \"role\": \"Research Scientist\", \"org\": \"OpenAI\", \"field\": \"LLMs\"},\n", + " {\"name\": \"Eva Müller\", \"role\": \"Systems Architect\", \"org\": \"Synthex\", \"field\": \"Graph Databases\"},\n", + " {\"name\": \"Frank Liu\", \"role\": \"Principal Engineer\", \"org\": \"Meta\", \"field\": \"Graph ML\"},\n", + " {\"name\": \"Grace Okafor\", \"role\": \"PhD Researcher\", \"org\": \"MIT\", \"field\": \"Knowledge Graphs\"},\n", + " {\"name\": \"Henry Rossi\", \"role\": \"CTO\", \"org\": \"Synthex\", \"field\": \"Databases\"},\n", + " {\"name\": \"Isla Nakamura\", \"role\": \"Senior Researcher\", \"org\": \"DeepMind\", \"field\": \"Graph Neural Networks\"},\n", + " {\"name\": \"James Wright\", \"role\": \"Engineering Lead\", \"org\": \"Google\", \"field\": \"Search\"},\n", + "]\n", + "\n", + "for p in people:\n", + " client.cypher(\"MERGE (n:Person {name: $name}) SET n.role = $role, n.field = $field, n.demo = true\", params=p)\n", + "\n", + "print(f\"Created {len(people)} people\")\n", + "\n", + "# ── Companies ─────────────────────────────────────────────────────────────\n", + "companies = [\n", + " {\"name\": \"Google\", \"industry\": \"Technology\", \"founded\": 1998, \"hq\": \"Mountain View\"},\n", + " {\"name\": \"Meta\", \"industry\": \"Technology\", \"founded\": 2004, \"hq\": \"Menlo Park\"},\n", + " {\"name\": \"OpenAI\", \"industry\": \"AI Research\", \"founded\": 2015, \"hq\": \"San Francisco\"},\n", + " {\"name\": \"DeepMind\", \"industry\": \"AI Research\", \"founded\": 2010, \"hq\": \"London\"},\n", + " {\"name\": \"Synthex\", \"industry\": \"AI Startup\", \"founded\": 2021, \"hq\": \"Berlin\"},\n", + " {\"name\": \"MIT\", \"industry\": \"Academia\", \"founded\": 1861, \"hq\": \"Cambridge\"},\n", + "]\n", + "\n", + "for c in companies:\n", + " client.cypher(\n", + " \"MERGE (n:Company {name: $name, demo_tag: $tag}) SET n.industry = $industry, n.founded = $founded, n.hq = $hq, n.demo = true, n.demo_tag = $tag\",\n", + " params={**c, \"tag\": DEMO_TAG},\n", + " )\n", + "\n", + "print(f\"Created {len(companies)} companies\")\n", + "\n", + "# ── Technologies ──────────────────────────────────────────────────────────\n", + "technologies = [\n", + " {\"name\": \"Transformer\", \"type\": \"Architecture\", \"year\": 2017},\n", + " {\"name\": \"Graph Neural Network\", \"type\": \"Algorithm\", \"year\": 2009},\n", + " {\"name\": \"Reinforcement Learning\", \"type\": \"Paradigm\", \"year\": 1980},\n", + " {\"name\": \"Knowledge Graph\", \"type\": \"Data Model\", \"year\": 2012},\n", + " {\"name\": \"Vector Database\", \"type\": \"Infrastructure\", \"year\": 2019},\n", + " {\"name\": \"RAG\", \"type\": \"Technique\", \"year\": 2020},\n", + " {\"name\": \"LLM\", \"type\": \"Model Class\", \"year\": 2018},\n", + " {\"name\": \"GraphRAG\", \"type\": \"Technique\", \"year\": 2023},\n", + "]\n", + "\n", + "for t in technologies:\n", + " client.cypher(\n", + " \"MERGE (n:Technology {name: $name, demo_tag: $tag}) SET n.type = $type, n.year = $year, n.demo = true, n.demo_tag = $tag\",\n", + " params={**t, \"tag\": DEMO_TAG},\n", + " )\n", + "\n", + "print(f\"Created {len(technologies)} technologies\")" + ] + }, + { + "cell_type": "markdown", + "id": "a1b2c3d4-0000-0000-0000-000000000010", + "metadata": {}, + "source": [ + "## Step 3 — Create relationships" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a1b2c3d4-0000-0000-0000-000000000011", + "metadata": {}, + "outputs": [], + "source": [ + "edges = [\n", + " # WORKS_AT\n", + " (\"Alice Chen\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"Bob Torres\", \"WORKS_AT\", \"Google\", {}),\n", + " (\"Carol Smith\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"David Park\", \"WORKS_AT\", \"OpenAI\", {}),\n", + " (\"Eva Müller\", \"WORKS_AT\", \"Synthex\", {\"since\": 2022}),\n", + " (\"Frank Liu\", \"WORKS_AT\", \"Meta\", {}),\n", + " (\"Grace Okafor\", \"WORKS_AT\", \"MIT\", {}),\n", + " (\"Henry Rossi\", \"WORKS_AT\", \"Synthex\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"WORKS_AT\", \"DeepMind\", {}),\n", + " (\"James Wright\", \"WORKS_AT\", \"Google\", {}),\n", + " # FOUNDED\n", + " (\"Carol Smith\", \"FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", + " (\"Henry Rossi\", \"CO_FOUNDED\", \"Synthex\", {\"year\": 2021}),\n", + " # KNOWS\n", + " (\"Alice Chen\", \"KNOWS\", \"Isla Nakamura\", {}),\n", + " (\"Alice Chen\", \"KNOWS\", \"David Park\", {}),\n", + " (\"Carol Smith\", \"KNOWS\", \"Bob Torres\", {}),\n", + " (\"Grace Okafor\", \"KNOWS\", \"Alice Chen\", {}),\n", + " (\"Frank Liu\", \"KNOWS\", \"James Wright\", {}),\n", + " (\"Eva Müller\", \"KNOWS\", \"Grace Okafor\", {}),\n", + " # RESEARCHES / WORKS_ON\n", + " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", + " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"Knowledge Graph\", {\"since\": 2021}),\n", + " (\"Isla Nakamura\", \"RESEARCHES\", \"Graph Neural Network\", {\"since\": 2020}),\n", + " (\"Frank Liu\", \"RESEARCHES\", \"Graph Neural Network\", {}),\n", + " (\"Grace Okafor\", \"RESEARCHES\", \"GraphRAG\", {\"since\": 2023}),\n", + " # USES\n", + " (\"Synthex\", \"USES\", \"Knowledge Graph\", {}),\n", + " (\"Synthex\", \"USES\", \"Vector Database\", {}),\n", + " (\"Synthex\", \"USES\", \"RAG\", {}),\n", + " (\"OpenAI\", \"USES\", \"Transformer\", {}),\n", + " (\"Google\", \"USES\", \"Transformer\", {}),\n", + " # ACQUIRED\n", + " (\"Google\", \"ACQUIRED\", \"DeepMind\", {\"year\": 2014}),\n", + " # BUILDS_ON\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"Knowledge Graph\", {}),\n", + " (\"GraphRAG\", \"BUILDS_ON\", \"RAG\", {}),\n", + " (\"RAG\", \"BUILDS_ON\", \"Vector Database\", {}),\n", + " (\"LLM\", \"BUILDS_ON\", \"Transformer\", {}),\n", + "]\n", + "\n", + "src_names = {p[\"name\"] for p in people}\n", + "tech_names = {t[\"name\"] for t in technologies}\n", + "company_names = {c[\"name\"] for c in companies}\n", + "\n", + "\n", + "def _label(name):\n", + " if name in src_names:\n", + " return \"Person\"\n", + " if name in tech_names:\n", + " return \"Technology\"\n", + " if name in company_names:\n", + " return \"Company\"\n", + " raise ValueError(f\"Unknown edge endpoint: {name!r}\")\n", + "\n", + "\n", + "for src, rel, dst, props in edges:\n", + " src_label = _label(src)\n", + " dst_label = _label(dst)\n", + " set_clause = \", \".join(f\"r.{k} = ${k}\" for k in props) if props else \"\"\n", + " set_part = f\" SET {set_clause}\" if set_clause else \"\"\n", + " client.cypher(\n", + " f\"MATCH (a:{src_label} {{name: $src, demo_tag: $tag}}) \"\n", + " f\"MATCH (b:{dst_label} {{name: $dst, demo_tag: $tag}}) \"\n", + " f\"MERGE (a)-[r:{rel}]->(b)\" + set_part,\n", + " params={\"src\": src, \"dst\": dst, \"tag\": DEMO_TAG, **props},\n", + " )\n", + "\n", + "print(f\"Created {len(edges)} relationships\")" ] }, { @@ -9269,4 +1489,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 762d9c3..c784691 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -40,1967 +40,67 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "y", - "s", - ",", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - "\n", - "\n", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - " ", - "=", - " ", - "\"", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "l", - "a", - "b", - "\"", - " ", - "i", - "n", - " ", - "s", - "y", - "s", - ".", - "m", - "o", - "d", - "u", - "l", - "e", - "s", - "\n", - "\n", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "n", - " ", - "C", - "o", - "l", - "a", - "b", - " ", - "o", - "n", - "l", - "y", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - ")", - ".", - "\n", - "i", - "f", - " ", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "R", - "u", - "s", - "t", - " ", - "t", - "o", - "o", - "l", - "c", - "h", - "a", - "i", - "n", - " ", - "v", - "i", - "a", - " ", - "r", - "u", - "s", - "t", - "u", - "p", - " ", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "C", - "o", - "l", - "a", - "b", - "'", - "s", - " ", - "a", - "p", - "t", - " ", - "p", - "a", - "c", - "k", - "a", - "g", - "e", - "s", - " ", - "s", - "h", - "i", - "p", - " ", - "r", - "u", - "s", - "t", - "c", - " ", - "≤", - "1", - ".", - "7", - "5", - ",", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "c", - "a", - "n", - "n", - "o", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "≥", - "1", - ".", - "8", - "0", - " ", - "f", - "o", - "r", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "y", - "o", - "3", - ")", - ".", - " ", - "a", - "p", - "t", - "-", - "g", - "e", - "t", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "a", - " ", - "v", - "i", - "a", - "b", - "l", - "e", - " ", - "a", - "l", - "t", - "e", - "r", - "n", - "a", - "t", - "i", - "v", - "e", - " ", - "h", - "e", - "r", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "D", - "o", - "w", - "n", - "l", - "o", - "a", - "d", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "r", - " ", - "t", - "o", - " ", - "a", - " ", - "t", - "e", - "m", - "p", - " ", - "f", - "i", - "l", - "e", - " ", - "a", - "n", - "d", - " ", - "e", - "x", - "e", - "c", - "u", - "t", - "e", - " ", - "i", - "t", - " ", - "e", - "x", - "p", - "l", - "i", - "c", - "i", - "t", - "l", - "y", - " ", - "—", - " ", - "t", - "h", - "i", - "s", - " ", - "a", - "v", - "o", - "i", - "d", - "s", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "p", - "i", - "p", - "i", - "n", - "g", - " ", - "r", - "e", - "m", - "o", - "t", - "e", - " ", - "c", - "o", - "n", - "t", - "e", - "n", - "t", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "l", - "y", - " ", - "i", - "n", - "t", - "o", - " ", - "a", - " ", - "s", - "h", - "e", - "l", - "l", - " ", - "w", - "h", - "i", - "l", - "e", - " ", - "m", - "a", - "i", - "n", - "t", - "a", - "i", - "n", - "i", - "n", - "g", - " ", - "H", - "T", - "T", - "P", - "S", - "/", - "T", - "L", - "S", - " ", - "s", - "e", - "c", - "u", - "r", - "i", - "t", - "y", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "t", - "h", - "r", - "o", - "u", - "g", - "h", - " ", - "P", - "y", - "t", - "h", - "o", - "n", - "'", - "s", - " ", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - " ", - "s", - "s", - "l", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - " ", - "(", - "c", - "e", - "r", - "t", - "-", - "v", - "e", - "r", - "i", - "f", - "i", - "e", - "d", - ",", - " ", - "T", - "L", - "S", - " ", - "1", - ".", - "2", - "+", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "s", - "l", - " ", - "a", - "s", - " ", - "_", - "s", - "s", - "l", - ",", - " ", - "t", - "e", - "m", - "p", - "f", - "i", - "l", - "e", - " ", - "a", - "s", - " ", - "_", - "t", - "m", - "p", - ",", - " ", - "u", - "r", - "l", - "l", - "i", - "b", - ".", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - " ", - "a", - "s", - " ", - "_", - "u", - "r", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "t", - "x", - " ", - "=", - " ", - "_", - "s", - "s", - "l", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - "_", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "t", - "m", - "p", - ".", - "N", - "a", - "m", - "e", - "d", - "T", - "e", - "m", - "p", - "o", - "r", - "a", - "r", - "y", - "F", - "i", - "l", - "e", - "(", - "m", - "o", - "d", - "e", - "=", - "\"", - "w", - "b", - "\"", - ",", - " ", - "s", - "u", - "f", - "f", - "i", - "x", - "=", - "\"", - ".", - "s", - "h", - "\"", - ",", - " ", - "d", - "e", - "l", - "e", - "t", - "e", - "=", - "F", - "a", - "l", - "s", - "e", - ")", - " ", - "a", - "s", - " ", - "_", - "f", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "u", - "r", - ".", - "u", - "r", - "l", - "o", - "p", - "e", - "n", - "(", - "\"", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "s", - "h", - ".", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - "\"", - ",", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "=", - "_", - "c", - "t", - "x", - ")", - " ", - "a", - "s", - " ", - "_", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "f", - ".", - "w", - "r", - "i", - "t", - "e", - "(", - "_", - "r", - ".", - "r", - "e", - "a", - "d", - "(", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - " ", - "=", - " ", - "_", - "f", - ".", - "n", - "a", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "\"", - "/", - "b", - "i", - "n", - "/", - "s", - "h", - "\"", - ",", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ",", - " ", - "\"", - "-", - "s", - "\"", - ",", - " ", - "\"", - "-", - "-", - "\"", - ",", - " ", - "\"", - "-", - "y", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "f", - "i", - "n", - "a", - "l", - "l", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "u", - "n", - "l", - "i", - "n", - "k", - "(", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "d", - "d", - " ", - "c", - "a", - "r", - "g", - "o", - " ", - "t", - "o", - " ", - "P", - "A", - "T", - "H", - " ", - "s", - "o", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "i", - "p", - " ", - "c", - "a", - "n", - " ", - "f", - "i", - "n", - "d", - " ", - "i", - "t", - ".", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - " ", - "=", - " ", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - ".", - "e", - "x", - "p", - "a", - "n", - "d", - "u", - "s", - "e", - "r", - "(", - "\"", - "~", - "/", - ".", - "c", - "a", - "r", - "g", - "o", - "/", - "b", - "i", - "n", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "P", - "A", - "T", - "H", - "\"", - "]", - " ", - "=", - " ", - "f", - "\"", - "{", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - "}", - "{", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - "s", - "e", - "p", - "}", - "{", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "'", - "P", - "A", - "T", - "H", - "'", - ",", - " ", - "'", - "'", - ")", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - " ", - "\"", - "-", - "m", - "\"", - ",", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - ",", - " ", - "\"", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "l", - "a", - "m", - "a", - "-", - "i", - "n", - "d", - "e", - "x", - "-", - "g", - "r", - "a", - "p", - "h", - "-", - "s", - "t", - "o", - "r", - "e", - "s", - "-", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "l", - "a", - "m", - "a", - "-", - "i", - "n", - "d", - "e", - "x", - "-", - "c", - "o", - "r", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - ")", - "\n", - "\n", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - "\n", + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", "\n", - "n", - "e", - "s", - "t", - "_", - "a", - "s", - "y", - "n", - "c", - "i", - "o", - ".", - "a", - "p", - "p", - "l", - "y", - "(", - ")", + "import nest_asyncio\n", "\n", + "nest_asyncio.apply()\n", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "S", - "D", - "K", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "d", - "\"", - ")" + "print(\"SDK installed\")" ] }, { @@ -2280,4 +380,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index c19cbfb..758c5c0 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -37,1937 +37,64 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "y", - "s", - ",", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - "\n", - "\n", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - " ", - "=", - " ", - "\"", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "l", - "a", - "b", - "\"", - " ", - "i", - "n", - " ", - "s", - "y", - "s", - ".", - "m", - "o", - "d", - "u", - "l", - "e", - "s", - "\n", - "\n", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "n", - " ", - "C", - "o", - "l", - "a", - "b", - " ", - "o", - "n", - "l", - "y", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - ")", - ".", - "\n", - "i", - "f", - " ", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "R", - "u", - "s", - "t", - " ", - "t", - "o", - "o", - "l", - "c", - "h", - "a", - "i", - "n", - " ", - "v", - "i", - "a", - " ", - "r", - "u", - "s", - "t", - "u", - "p", - " ", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "C", - "o", - "l", - "a", - "b", - "'", - "s", - " ", - "a", - "p", - "t", - " ", - "p", - "a", - "c", - "k", - "a", - "g", - "e", - "s", - " ", - "s", - "h", - "i", - "p", - " ", - "r", - "u", - "s", - "t", - "c", - " ", - "≤", - "1", - ".", - "7", - "5", - ",", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "c", - "a", - "n", - "n", - "o", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "≥", - "1", - ".", - "8", - "0", - " ", - "f", - "o", - "r", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "y", - "o", - "3", - ")", - ".", - " ", - "a", - "p", - "t", - "-", - "g", - "e", - "t", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "a", - " ", - "v", - "i", - "a", - "b", - "l", - "e", - " ", - "a", - "l", - "t", - "e", - "r", - "n", - "a", - "t", - "i", - "v", - "e", - " ", - "h", - "e", - "r", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "D", - "o", - "w", - "n", - "l", - "o", - "a", - "d", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "r", - " ", - "t", - "o", - " ", - "a", - " ", - "t", - "e", - "m", - "p", - " ", - "f", - "i", - "l", - "e", - " ", - "a", - "n", - "d", - " ", - "e", - "x", - "e", - "c", - "u", - "t", - "e", - " ", - "i", - "t", - " ", - "e", - "x", - "p", - "l", - "i", - "c", - "i", - "t", - "l", - "y", - " ", - "—", - " ", - "t", - "h", - "i", - "s", - " ", - "a", - "v", - "o", - "i", - "d", - "s", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "p", - "i", - "p", - "i", - "n", - "g", - " ", - "r", - "e", - "m", - "o", - "t", - "e", - " ", - "c", - "o", - "n", - "t", - "e", - "n", - "t", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "l", - "y", - " ", - "i", - "n", - "t", - "o", - " ", - "a", - " ", - "s", - "h", - "e", - "l", - "l", - " ", - "w", - "h", - "i", - "l", - "e", - " ", - "m", - "a", - "i", - "n", - "t", - "a", - "i", - "n", - "i", - "n", - "g", - " ", - "H", - "T", - "T", - "P", - "S", - "/", - "T", - "L", - "S", - " ", - "s", - "e", - "c", - "u", - "r", - "i", - "t", - "y", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "t", - "h", - "r", - "o", - "u", - "g", - "h", - " ", - "P", - "y", - "t", - "h", - "o", - "n", - "'", - "s", - " ", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - " ", - "s", - "s", - "l", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - " ", - "(", - "c", - "e", - "r", - "t", - "-", - "v", - "e", - "r", - "i", - "f", - "i", - "e", - "d", - ",", - " ", - "T", - "L", - "S", - " ", - "1", - ".", - "2", - "+", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "s", - "l", - " ", - "a", - "s", - " ", - "_", - "s", - "s", - "l", - ",", - " ", - "t", - "e", - "m", - "p", - "f", - "i", - "l", - "e", - " ", - "a", - "s", - " ", - "_", - "t", - "m", - "p", - ",", - " ", - "u", - "r", - "l", - "l", - "i", - "b", - ".", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - " ", - "a", - "s", - " ", - "_", - "u", - "r", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "t", - "x", - " ", - "=", - " ", - "_", - "s", - "s", - "l", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - "_", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "t", - "m", - "p", - ".", - "N", - "a", - "m", - "e", - "d", - "T", - "e", - "m", - "p", - "o", - "r", - "a", - "r", - "y", - "F", - "i", - "l", - "e", - "(", - "m", - "o", - "d", - "e", - "=", - "\"", - "w", - "b", - "\"", - ",", - " ", - "s", - "u", - "f", - "f", - "i", - "x", - "=", - "\"", - ".", - "s", - "h", - "\"", - ",", - " ", - "d", - "e", - "l", - "e", - "t", - "e", - "=", - "F", - "a", - "l", - "s", - "e", - ")", - " ", - "a", - "s", - " ", - "_", - "f", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "u", - "r", - ".", - "u", - "r", - "l", - "o", - "p", - "e", - "n", - "(", - "\"", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "s", - "h", - ".", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - "\"", - ",", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "=", - "_", - "c", - "t", - "x", - ")", - " ", - "a", - "s", - " ", - "_", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "f", - ".", - "w", - "r", - "i", - "t", - "e", - "(", - "_", - "r", - ".", - "r", - "e", - "a", - "d", - "(", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - " ", - "=", - " ", - "_", - "f", - ".", - "n", - "a", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "\"", - "/", - "b", - "i", - "n", - "/", - "s", - "h", - "\"", - ",", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ",", - " ", - "\"", - "-", - "s", - "\"", - ",", - " ", - "\"", - "-", - "-", - "\"", - ",", - " ", - "\"", - "-", - "y", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "f", - "i", - "n", - "a", - "l", - "l", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "u", - "n", - "l", - "i", - "n", - "k", - "(", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "d", - "d", - " ", - "c", - "a", - "r", - "g", - "o", - " ", - "t", - "o", - " ", - "P", - "A", - "T", - "H", - " ", - "s", - "o", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "i", - "p", - " ", - "c", - "a", - "n", - " ", - "f", - "i", - "n", - "d", - " ", - "i", - "t", - ".", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - " ", - "=", - " ", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - ".", - "e", - "x", - "p", - "a", - "n", - "d", - "u", - "s", - "e", - "r", - "(", - "\"", - "~", - "/", - ".", - "c", - "a", - "r", - "g", - "o", - "/", - "b", - "i", - "n", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "P", - "A", - "T", - "H", - "\"", - "]", - " ", - "=", - " ", - "f", - "\"", - "{", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - "}", - "{", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - "s", - "e", - "p", - "}", - "{", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "'", - "P", - "A", - "T", - "H", - "'", - ",", - " ", - "'", - "'", - ")", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - " ", - "\"", - "-", - "m", - "\"", - ",", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - ",", - " ", - "\"", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "c", - "o", - "m", - "m", - "u", - "n", - "i", - "t", - "y", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "o", - "p", - "e", - "n", - "a", - "i", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - ")", - "\n", + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "S", - "D", - "K", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "d", - "\"", - ")" + "print(\"SDK installed\")" ] }, { @@ -2228,4 +355,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 82d0e61..d551141 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -38,1937 +38,64 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "y", - "s", - ",", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - "\n", - "\n", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - " ", - "=", - " ", - "\"", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "l", - "a", - "b", - "\"", - " ", - "i", - "n", - " ", - "s", - "y", - "s", - ".", - "m", - "o", - "d", - "u", - "l", - "e", - "s", - "\n", - "\n", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "n", - " ", - "C", - "o", - "l", - "a", - "b", - " ", - "o", - "n", - "l", - "y", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - ")", - ".", - "\n", - "i", - "f", - " ", - "I", - "N", - "_", - "C", - "O", - "L", - "A", - "B", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "I", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "R", - "u", - "s", - "t", - " ", - "t", - "o", - "o", - "l", - "c", - "h", - "a", - "i", - "n", - " ", - "v", - "i", - "a", - " ", - "r", - "u", - "s", - "t", - "u", - "p", - " ", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "C", - "o", - "l", - "a", - "b", - "'", - "s", - " ", - "a", - "p", - "t", - " ", - "p", - "a", - "c", - "k", - "a", - "g", - "e", - "s", - " ", - "s", - "h", - "i", - "p", - " ", - "r", - "u", - "s", - "t", - "c", - " ", - "≤", - "1", - ".", - "7", - "5", - ",", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "c", - "a", - "n", - "n", - "o", - "t", - " ", - "b", - "u", - "i", - "l", - "d", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "(", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "≥", - "1", - ".", - "8", - "0", - " ", - "f", - "o", - "r", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "y", - "o", - "3", - ")", - ".", - " ", - "a", - "p", - "t", - "-", - "g", - "e", - "t", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "a", - " ", - "v", - "i", - "a", - "b", - "l", - "e", - " ", - "a", - "l", - "t", - "e", - "r", - "n", - "a", - "t", - "i", - "v", - "e", - " ", - "h", - "e", - "r", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "D", - "o", - "w", - "n", - "l", - "o", - "a", - "d", - " ", - "t", - "h", - "e", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "r", - " ", - "t", - "o", - " ", - "a", - " ", - "t", - "e", - "m", - "p", - " ", - "f", - "i", - "l", - "e", - " ", - "a", - "n", - "d", - " ", - "e", - "x", - "e", - "c", - "u", - "t", - "e", - " ", - "i", - "t", - " ", - "e", - "x", - "p", - "l", - "i", - "c", - "i", - "t", - "l", - "y", - " ", - "—", - " ", - "t", - "h", - "i", - "s", - " ", - "a", - "v", - "o", - "i", - "d", - "s", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "p", - "i", - "p", - "i", - "n", - "g", - " ", - "r", - "e", - "m", - "o", - "t", - "e", - " ", - "c", - "o", - "n", - "t", - "e", - "n", - "t", - " ", - "d", - "i", - "r", - "e", - "c", - "t", - "l", - "y", - " ", - "i", - "n", - "t", - "o", - " ", - "a", - " ", - "s", - "h", - "e", - "l", - "l", - " ", - "w", - "h", - "i", - "l", - "e", - " ", - "m", - "a", - "i", - "n", - "t", - "a", - "i", - "n", - "i", - "n", - "g", - " ", - "H", - "T", - "T", - "P", - "S", - "/", - "T", - "L", - "S", - " ", - "s", - "e", - "c", - "u", - "r", - "i", - "t", - "y", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "t", - "h", - "r", - "o", - "u", - "g", - "h", - " ", - "P", - "y", - "t", - "h", - "o", - "n", - "'", - "s", - " ", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - " ", - "s", - "s", - "l", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - " ", - "(", - "c", - "e", - "r", - "t", - "-", - "v", - "e", - "r", - "i", - "f", - "i", - "e", - "d", - ",", - " ", - "T", - "L", - "S", - " ", - "1", - ".", - "2", - "+", - ")", - ".", - "\n", - " ", - " ", - " ", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "s", - "s", - "l", - " ", - "a", - "s", - " ", - "_", - "s", - "s", - "l", - ",", - " ", - "t", - "e", - "m", - "p", - "f", - "i", - "l", - "e", - " ", - "a", - "s", - " ", - "_", - "t", - "m", - "p", - ",", - " ", - "u", - "r", - "l", - "l", - "i", - "b", - ".", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - " ", - "a", - "s", - " ", - "_", - "u", - "r", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "t", - "x", - " ", - "=", - " ", - "_", - "s", - "s", - "l", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "d", - "e", - "f", - "a", - "u", - "l", - "t", - "_", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "t", - "m", - "p", - ".", - "N", - "a", - "m", - "e", - "d", - "T", - "e", - "m", - "p", - "o", - "r", - "a", - "r", - "y", - "F", - "i", - "l", - "e", - "(", - "m", - "o", - "d", - "e", - "=", - "\"", - "w", - "b", - "\"", - ",", - " ", - "s", - "u", - "f", - "f", - "i", - "x", - "=", - "\"", - ".", - "s", - "h", - "\"", - ",", - " ", - "d", - "e", - "l", - "e", - "t", - "e", - "=", - "F", - "a", - "l", - "s", - "e", - ")", - " ", - "a", - "s", - " ", - "_", - "f", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "_", - "u", - "r", - ".", - "u", - "r", - "l", - "o", - "p", - "e", - "n", - "(", - "\"", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "s", - "h", - ".", - "r", - "u", - "s", - "t", - "u", - "p", - ".", - "r", - "s", - "\"", - ",", - " ", - "c", - "o", - "n", - "t", - "e", - "x", - "t", - "=", - "_", - "c", - "t", - "x", - ")", - " ", - "a", - "s", - " ", - "_", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "f", - ".", - "w", - "r", - "i", - "t", - "e", - "(", - "_", - "r", - ".", - "r", - "e", - "a", - "d", - "(", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - " ", - "=", - " ", - "_", - "f", - ".", - "n", - "a", - "m", - "e", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "\"", - "/", - "b", - "i", - "n", - "/", - "s", - "h", - "\"", - ",", - " ", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ",", - " ", - "\"", - "-", - "s", - "\"", - ",", - " ", - "\"", - "-", - "-", - "\"", - ",", - " ", - "\"", - "-", - "y", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "f", - "i", - "n", - "a", - "l", - "l", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "u", - "n", - "l", - "i", - "n", - "k", - "(", - "_", - "r", - "u", - "s", - "t", - "u", - "p", - "_", - "p", - "a", - "t", - "h", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "d", - "d", - " ", - "c", - "a", - "r", - "g", - "o", - " ", - "t", - "o", - " ", - "P", - "A", - "T", - "H", - " ", - "s", - "o", - " ", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "/", - "p", - "i", - "p", - " ", - "c", - "a", - "n", - " ", - "f", - "i", - "n", - "d", - " ", - "i", - "t", - ".", - "\n", - " ", - " ", - " ", - " ", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - " ", - "=", - " ", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - ".", - "e", - "x", - "p", - "a", - "n", - "d", - "u", - "s", - "e", - "r", - "(", - "\"", - "~", - "/", - ".", - "c", - "a", - "r", - "g", - "o", - "/", - "b", - "i", - "n", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "P", - "A", - "T", - "H", - "\"", - "]", - " ", - "=", - " ", - "f", - "\"", - "{", - "_", - "c", - "a", - "r", - "g", - "o", - "_", - "b", - "i", - "n", - "}", - "{", - "o", - "s", - ".", - "p", - "a", - "t", - "h", - "s", - "e", - "p", - "}", - "{", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "'", - "P", - "A", - "T", - "H", - "'", - ",", - " ", - "'", - "'", - ")", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "[", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - " ", - "\"", - "-", - "m", - "\"", - ",", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - " ", - "\"", - "-", - "q", - "\"", - ",", - " ", - "\"", - "m", - "a", - "t", - "u", - "r", - "i", - "n", - "\"", - "]", - ",", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ")", - "\n", - " ", - " ", - " ", - " ", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "\n", - "s", - "u", - "b", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ".", - "r", - "u", - "n", - "(", - "\n", - " ", - " ", - " ", - " ", - "[", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "s", - "y", - "s", - ".", - "e", - "x", - "e", - "c", - "u", - "t", - "a", - "b", - "l", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "m", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "p", - "i", - "p", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "-", - "q", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "c", - "o", - "m", - "m", - "u", - "n", - "i", - "t", - "y", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "-", - "o", - "p", - "e", - "n", - "a", - "i", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "l", - "a", - "n", - "g", - "g", - "r", - "a", - "p", - "h", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "]", - ",", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "e", - "c", - "k", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - ")", - "\n", + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "S", - "D", - "K", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "d", - "\"", - ")" + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " \"langgraph\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "print(\"SDK installed\")" ] }, { @@ -2008,3667 +135,91 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "r", - "e", - ",", - " ", - "u", - "u", - "i", - "d", - "\n", - "f", - "r", - "o", - "m", - " ", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "_", - "c", - "o", - "r", - "e", - ".", - "t", - "o", - "o", - "l", - "s", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "t", - "o", - "o", - "l", - "\n", - "\n", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - " ", - "=", - " ", - "u", - "u", - "i", - "d", - ".", - "u", - "u", - "i", - "d", - "4", - "(", - ")", - ".", - "h", - "e", - "x", - "[", - ":", - "8", - "]", - " ", - " ", - "#", - " ", - "i", - "s", - "o", - "l", - "a", - "t", - "e", - "s", - " ", - "t", - "h", - "i", - "s", - " ", - "d", - "e", - "m", - "o", - "'", - "s", - " ", - "d", - "a", - "t", - "a", - " ", - "f", - "r", - "o", - "m", - " ", - "o", - "t", - "h", - "e", - "r", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - "s", - "\n", - "\n", - "_", - "R", - "E", - "L", - "_", - "T", - "Y", - "P", - "E", - "_", - "R", - "E", - " ", - "=", - " ", - "r", - "e", - ".", - "c", - "o", - "m", - "p", - "i", - "l", - "e", - "(", - "r", - "\"", - "[", - "A", - "-", - "Z", - "_", - "]", - "[", - "A", - "-", - "Z", - "0", - "-", - "9", - "_", - "]", - "*", - "\"", - ")", - "\n", - "#", - " ", - "R", - "e", - "g", - "e", - "x", - " ", - "g", - "u", - "a", - "r", - "d", - "s", - " ", - "f", - "o", - "r", - " ", - "q", - "u", - "e", - "r", - "y", - "_", - "f", - "a", - "c", - "t", - "s", - " ", - "(", - "d", - "e", - "m", - "o", - " ", - "s", - "a", - "f", - "e", - "t", - "y", - " ", - "g", - "u", - "a", - "r", - "d", - ")", - ".", - "\n", - "_", - "W", - "R", - "I", - "T", - "E", - "_", - "C", - "L", - "A", - "U", - "S", - "E", - "_", - "R", - "E", - " ", - "=", - " ", - "r", - "e", - ".", - "c", - "o", - "m", - "p", - "i", - "l", - "e", - "(", - "\n", - " ", - " ", - " ", - " ", - "r", - "\"", - "\\", - "b", - "(", - "C", - "R", - "E", - "A", - "T", - "E", - "|", - "M", - "E", - "R", - "G", - "E", - "|", - "D", - "E", - "L", - "E", - "T", - "E", - "|", - "D", - "E", - "T", - "A", - "C", - "H", - "|", - "S", - "E", - "T", - "|", - "R", - "E", - "M", - "O", - "V", - "E", - "|", - "D", - "R", - "O", - "P", - "|", - "C", - "A", - "L", - "L", - "|", - "L", - "O", - "A", - "D", - ")", - "\\", - "b", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - ".", - "I", - "G", - "N", - "O", - "R", - "E", - "C", - "A", - "S", - "E", - ",", - "\n", - ")", - "\n", - "#", - " ", - "N", - "O", - "T", - "E", - ":", - " ", - "t", - "h", - "i", - "s", - " ", - "g", - "u", - "a", - "r", - "d", - " ", - "c", - "h", - "e", - "c", - "k", - "s", - " ", - "t", - "h", - "a", - "t", - " ", - "A", - "T", - " ", - "L", - "E", - "A", - "S", - "T", - " ", - "O", - "N", - "E", - " ", - "n", - "o", - "d", - "e", - " ", - "p", - "a", - "t", - "t", - "e", - "r", - "n", - " ", - "c", - "a", - "r", - "r", - "i", - "e", - "s", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - " ", - "s", - "c", - "o", - "p", - "e", - ".", - "\n", - "#", - " ", - "A", - " ", - "C", - "a", - "r", - "t", - "e", - "s", - "i", - "a", - "n", - "-", - "p", - "r", - "o", - "d", - "u", - "c", - "t", - " ", - "q", - "u", - "e", - "r", - "y", - " ", - "s", - "u", - "c", - "h", - " ", - "a", - "s", - " ", - "`", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - ")", - ",", - " ", - "(", - "m", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - ")", - " ", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "n", - "`", - "\n", - "#", - " ", - "w", - "o", - "u", - "l", - "d", - " ", - "p", - "a", - "s", - "s", - " ", - "y", - "e", - "t", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "u", - "n", - "s", - "c", - "o", - "p", - "e", - "d", - " ", - "r", - "o", - "w", - "s", - " ", - "f", - "o", - "r", - " ", - "`", - "n", - "`", - ".", - " ", - " ", - "A", - " ", - "c", - "o", - "m", - "p", - "l", - "e", - "t", - "e", - " ", - "p", - "e", - "r", - "-", - "a", - "l", - "i", - "a", - "s", - " ", - "c", - "h", - "e", - "c", - "k", - " ", - "w", - "o", - "u", - "l", - "d", - "\n", - "#", - " ", - "r", - "e", - "q", - "u", - "i", - "r", - "e", - " ", - "p", - "a", - "r", - "s", - "i", - "n", - "g", - " ", - "t", - "h", - "e", - " ", - "C", - "y", - "p", - "h", - "e", - "r", - " ", - "A", - "S", - "T", - ",", - " ", - "w", - "h", - "i", - "c", - "h", - " ", - "i", - "s", - " ", - "o", - "u", - "t", - " ", - "o", - "f", - " ", - "s", - "c", - "o", - "p", - "e", - " ", - "f", - "o", - "r", - " ", - "a", - " ", - "d", - "e", - "m", - "o", - " ", - "s", - "a", - "f", - "e", - "t", - "y", - " ", - "g", - "u", - "a", - "r", - "d", - ".", - "\n", - "#", - " ", - "I", - "n", - " ", - "p", - "r", - "o", - "d", - "u", - "c", - "t", - "i", - "o", - "n", - " ", - "c", - "o", - "d", - "e", - ",", - " ", - "u", - "s", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - "-", - "s", - "i", - "d", - "e", - " ", - "r", - "o", - "w", - "-", - "l", - "e", - "v", - "e", - "l", - " ", - "s", - "e", - "c", - "u", - "r", - "i", - "t", - "y", - " ", - "i", - "n", - "s", - "t", - "e", - "a", - "d", - " ", - "o", - "f", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "r", - "e", - "g", - "e", - "x", - ".", - "\n", - "_", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "_", - "S", - "C", - "O", - "P", - "E", - "_", - "R", - "E", - " ", - "=", - " ", - "r", - "e", - ".", - "c", - "o", - "m", - "p", - "i", - "l", - "e", - "(", - "\n", - " ", - " ", - " ", - " ", - "r", - "\"", - "W", - "H", - "E", - "R", - "E", - "\\", - "b", - "[", - "^", - ";", - "{", - "}", - "]", - "*", - "\\", - ".", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - "\\", - "s", - "*", - "=", - "\\", - "s", - "*", - "\\", - "$", - "s", - "e", - "s", - "s", - "|", - "\\", - "{", - "[", - "^", - "}", - "]", - "*", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - "\\", - "s", - "*", - ":", - "\\", - "s", - "*", - "\\", - "$", - "s", - "e", - "s", - "s", - "[", - "^", - "}", - "]", - "*", - "\\", - "}", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - ".", - "I", - "G", - "N", - "O", - "R", - "E", - "C", - "A", - "S", - "E", - ",", - "\n", - ")", - "\n", - "\n", - "\n", - "@", - "t", - "o", - "o", - "l", - "\n", - "d", - "e", - "f", - " ", - "s", - "a", - "v", - "e", - "_", - "f", - "a", - "c", - "t", - "(", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - ":", - " ", - "s", - "t", - "r", - ",", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - ":", - " ", - "s", - "t", - "r", - ",", - " ", - "o", - "b", - "j", - ":", - " ", - "s", - "t", - "r", - ")", - " ", - "-", - ">", - " ", - "s", - "t", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "S", - "a", - "v", - "e", - " ", - "a", - " ", - "f", - "a", - "c", - "t", - " ", - "(", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - " ", - "→", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - " ", - "→", - " ", - "o", - "b", - "j", - "e", - "c", - "t", - ")", - " ", - "i", - "n", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "k", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "g", - "r", - "a", - "p", - "h", - ".", - "\n", - " ", - " ", - " ", - " ", - "E", - "x", - "a", - "m", - "p", - "l", - "e", - ":", - " ", - "s", - "a", - "v", - "e", - "_", - "f", - "a", - "c", - "t", - "(", - "'", - "A", - "l", - "i", - "c", - "e", - "'", - ",", - " ", - "'", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - "'", - ",", - " ", - "'", - "A", - "c", - "m", - "e", - " ", - "C", - "o", - "r", - "p", - "'", - ")", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "l", - "_", - "t", - "y", - "p", - "e", - " ", - "=", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - ".", - "u", - "p", - "p", - "e", - "r", - "(", - ")", - ".", - "r", - "e", - "p", - "l", - "a", - "c", - "e", - "(", - "\"", - " ", - "\"", - ",", - " ", - "\"", - "_", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "V", - "a", - "l", - "i", - "d", - "a", - "t", - "e", - " ", - "r", - "e", - "l", - "_", - "t", - "y", - "p", - "e", - " ", - "b", - "e", - "f", - "o", - "r", - "e", - " ", - "i", - "n", - "t", - "e", - "r", - "p", - "o", - "l", - "a", - "t", - "i", - "n", - "g", - " ", - "i", - "n", - "t", - "o", - " ", - "C", - "y", - "p", - "h", - "e", - "r", - " ", - "t", - "o", - " ", - "p", - "r", - "e", - "v", - "e", - "n", - "t", - " ", - "i", - "n", - "j", - "e", - "c", - "t", - "i", - "o", - "n", - ".", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "_", - "R", - "E", - "L", - "_", - "T", - "Y", - "P", - "E", - "_", - "R", - "E", - ".", - "f", - "u", - "l", - "l", - "m", - "a", - "t", - "c", - "h", - "(", - "r", - "e", - "l", - "_", - "t", - "y", - "p", - "e", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "f", - "\"", - "I", - "n", - "v", - "a", - "l", - "i", - "d", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - " ", - "t", - "y", - "p", - "e", - " ", - "{", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - "!", - "r", - "}", - ":", - " ", - "o", - "n", - "l", - "y", - " ", - "l", - "e", - "t", - "t", - "e", - "r", - "s", - ",", - " ", - "d", - "i", - "g", - "i", - "t", - "s", - ",", - " ", - "a", - "n", - "d", - " ", - "u", - "n", - "d", - "e", - "r", - "s", - "c", - "o", - "r", - "e", - "s", - " ", - "a", - "l", - "l", - "o", - "w", - "e", - "d", - "\"", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "a", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "s", - ",", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "b", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "o", - ",", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "E", - "R", - "G", - "E", - " ", - "(", - "a", - ")", - "-", - "[", - "r", - ":", - "{", - "r", - "e", - "l", - "_", - "t", - "y", - "p", - "e", - "}", - "]", - "-", - ">", - "(", - "b", - ")", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "s", - "\"", - ":", - " ", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - ",", - " ", - "\"", - "o", - "\"", - ":", - " ", - "o", - "b", - "j", - ",", - " ", - "\"", - "s", - "e", - "s", - "s", - "\"", - ":", - " ", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "f", - "\"", - "S", - "a", - "v", - "e", - "d", - ":", - " ", - "{", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - "}", - " ", - "-", - "[", - "{", - "r", - "e", - "l", - "_", - "t", - "y", - "p", - "e", - "}", - "]", - "-", - ">", - " ", - "{", - "o", - "b", - "j", - "}", - "\"", - "\n", - "\n", - "\n", - "@", - "t", - "o", - "o", - "l", - "\n", - "d", - "e", - "f", - " ", - "q", - "u", - "e", - "r", - "y", - "_", - "f", - "a", - "c", - "t", - "s", - "(", - "c", - "y", - "p", - "h", - "e", - "r", - ":", - " ", - "s", - "t", - "r", - ")", - " ", - "-", - ">", - " ", - "s", - "t", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "R", - "u", - "n", - " ", - "a", - " ", - "r", - "e", - "a", - "d", - "-", - "o", - "n", - "l", - "y", - " ", - "C", - "y", - "p", - "h", - "e", - "r", - " ", - "M", - "A", - "T", - "C", - "H", - " ", - "q", - "u", - "e", - "r", - "y", - " ", - "a", - "g", - "a", - "i", - "n", - "s", - "t", - " ", - "t", - "h", - "e", - " ", - "k", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "g", - "r", - "a", - "p", - "h", - ".", - "\n", - " ", - " ", - " ", - " ", - "M", - "u", - "s", - "t", - " ", - "s", - "c", - "o", - "p", - "e", - " ", - "r", - "e", - "a", - "d", - "s", - " ", - "v", - "i", - "a", - " ", - "e", - "i", - "t", - "h", - "e", - "r", - " ", - "W", - "H", - "E", - "R", - "E", - " ", - "<", - "a", - "l", - "i", - "a", - "s", - ">", - ".", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - " ", - "=", - " ", - "$", - "s", - "e", - "s", - "s", - "\n", - " ", - " ", - " ", - " ", - "o", - "r", - " ", - "a", - " ", - "n", - "o", - "d", - "e", - " ", - "p", - "a", - "t", - "t", - "e", - "r", - "n", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - ".", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "q", - " ", - "=", - " ", - "c", - "y", - "p", - "h", - "e", - "r", - ".", - "s", - "t", - "r", - "i", - "p", - "(", - ")", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "_", - "W", - "R", - "I", - "T", - "E", - "_", - "C", - "L", - "A", - "U", - "S", - "E", - "_", - "R", - "E", - ".", - "s", - "e", - "a", - "r", - "c", - "h", - "(", - "q", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "O", - "n", - "l", - "y", - " ", - "r", - "e", - "a", - "d", - "-", - "o", - "n", - "l", - "y", - " ", - "C", - "y", - "p", - "h", - "e", - "r", - " ", - "i", - "s", - " ", - "a", - "l", - "l", - "o", - "w", - "e", - "d", - " ", - "i", - "n", - " ", - "q", - "u", - "e", - "r", - "y", - "_", - "f", - "a", - "c", - "t", - "s", - ".", - "\"", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "R", - "e", - "q", - "u", - "i", - "r", - "e", - " ", - "$", - "s", - "e", - "s", - "s", - " ", - "i", - "n", - " ", - "a", - " ", - "W", - "H", - "E", - "R", - "E", - " ", - "c", - "l", - "a", - "u", - "s", - "e", - " ", - "o", - "r", - " ", - "n", - "o", - "d", - "e", - " ", - "p", - "a", - "t", - "t", - "e", - "r", - "n", - ",", - " ", - "n", - "o", - "t", - " ", - "j", - "u", - "s", - "t", - " ", - "a", - "n", - "y", - "w", - "h", - "e", - "r", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "A", - "c", - "c", - "e", - "p", - "t", - "s", - " ", - "b", - "o", - "t", - "h", - ":", - " ", - "W", - "H", - "E", - "R", - "E", - " ", - "n", - ".", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - " ", - "=", - " ", - "$", - "s", - "e", - "s", - "s", - " ", - " ", - "a", - "n", - "d", - " ", - " ", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - ")", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "_", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "_", - "S", - "C", - "O", - "P", - "E", - "_", - "R", - "E", - ".", - "s", - "e", - "a", - "r", - "c", - "h", - "(", - "q", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "Q", - "u", - "e", - "r", - "y", - " ", - "m", - "u", - "s", - "t", - " ", - "s", - "c", - "o", - "p", - "e", - " ", - "r", - "e", - "a", - "d", - "s", - " ", - "t", - "o", - " ", - "t", - "h", - "e", - " ", - "c", - "u", - "r", - "r", - "e", - "n", - "t", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - " ", - "w", - "i", - "t", - "h", - " ", - "e", - "i", - "t", - "h", - "e", - "r", - " ", - "W", - "H", - "E", - "R", - "E", - " ", - "<", - "a", - "l", - "i", - "a", - "s", - ">", - ".", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - " ", - "=", - " ", - "$", - "s", - "e", - "s", - "s", - " ", - "o", - "r", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "o", - "w", - "s", - " ", - "=", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "q", - ",", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "s", - "e", - "s", - "s", - "\"", - ":", - " ", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "}", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "s", - "t", - "r", - "(", - "r", - "o", - "w", - "s", - "[", - ":", - "2", - "0", - "]", - ")", - " ", - "i", - "f", - " ", - "r", - "o", - "w", - "s", - " ", - "e", - "l", - "s", - "e", - " ", - "\"", - "N", - "o", - " ", - "r", - "e", - "s", - "u", - "l", - "t", - "s", - "\"", - "\n", - "\n", - "\n", - "@", - "t", - "o", - "o", - "l", - "\n", - "d", - "e", - "f", - " ", - "f", - "i", - "n", - "d", - "_", - "r", - "e", - "l", - "a", - "t", - "e", - "d", - "(", - "e", - "n", - "t", - "i", - "t", - "y", - "_", - "n", - "a", - "m", - "e", - ":", - " ", - "s", - "t", - "r", - ",", - " ", - "d", - "e", - "p", - "t", - "h", - ":", - " ", - "i", - "n", - "t", - " ", - "=", - " ", - "1", - ")", - " ", - "-", - ">", - " ", - "s", - "t", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "F", - "i", - "n", - "d", - " ", - "a", - "l", - "l", - " ", - "e", - "n", - "t", - "i", - "t", - "i", - "e", - "s", - " ", - "r", - "e", - "a", - "c", - "h", - "a", - "b", - "l", - "e", - " ", - "f", - "r", - "o", - "m", - " ", - "e", - "n", - "t", - "i", - "t", - "y", - "_", - "n", - "a", - "m", - "e", - " ", - "w", - "i", - "t", - "h", - "i", - "n", - " ", - "t", - "h", - "e", - " ", - "g", - "i", - "v", - "e", - "n", - " ", - "n", - "u", - "m", - "b", - "e", - "r", - " ", - "o", - "f", - " ", - "h", - "o", - "p", - "s", - " ", - "(", - "m", - "a", - "x", - " ", - "3", - ")", - ".", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "s", - "a", - "f", - "e", - "_", - "d", - "e", - "p", - "t", - "h", - " ", - "=", - " ", - "m", - "a", - "x", - "(", - "1", - ",", - " ", - "m", - "i", - "n", - "(", - "i", - "n", - "t", - "(", - "d", - "e", - "p", - "t", - "h", - ")", - ",", - " ", - "3", - ")", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "o", - "w", - "s", - " ", - "=", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "n", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "{", - "n", - "a", - "m", - "e", - ":", - " ", - "$", - "n", - "a", - "m", - "e", - ",", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - "}", - ")", - "-", - "[", - "r", - "*", - "1", - ".", - ".", - "{", - "s", - "a", - "f", - "e", - "_", - "d", - "e", - "p", - "t", - "h", - "}", - "]", - "-", - ">", - "(", - "m", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "m", - ".", - "n", - "a", - "m", - "e", - " ", - "A", - "S", - " ", - "r", - "e", - "l", - "a", - "t", - "e", - "d", - ",", - " ", - "t", - "y", - "p", - "e", - "(", - "l", - "a", - "s", - "t", - "(", - "r", - ")", - ")", - " ", - "A", - "S", - " ", - "v", - "i", - "a", - " ", - "L", - "I", - "M", - "I", - "T", - " ", - "2", - "0", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "n", - "a", - "m", - "e", - "\"", - ":", - " ", - "e", - "n", - "t", - "i", - "t", - "y", - "_", - "n", - "a", - "m", - "e", - ",", - " ", - "\"", - "s", - "e", - "s", - "s", - "\"", - ":", - " ", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "r", - "o", - "w", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "f", - "\"", - "N", - "o", - " ", - "r", - "e", - "l", - "a", - "t", - "e", - "d", - " ", - "e", - "n", - "t", - "i", - "t", - "i", - "e", - "s", - " ", - "f", - "o", - "u", - "n", - "d", - " ", - "f", - "o", - "r", - " ", - "{", - "e", - "n", - "t", - "i", - "t", - "y", - "_", - "n", - "a", - "m", - "e", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "\\", - "n", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "f", - "\"", - "{", - "r", - "[", - "'", - "v", - "i", - "a", - "'", - "]", - "}", - " ", - "-", - ">", - " ", - "{", - "r", - "[", - "'", - "r", - "e", - "l", - "a", - "t", - "e", - "d", - "'", - "]", - "}", - "\"", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "r", - "o", - "w", - "s", - ")", - "\n", - "\n", - "\n", - "@", - "t", - "o", - "o", - "l", - "\n", - "d", - "e", - "f", - " ", - "l", - "i", - "s", - "t", - "_", - "a", - "l", - "l", - "_", - "f", - "a", - "c", - "t", - "s", - "(", - ")", - " ", - "-", - ">", - " ", - "s", - "t", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - "\"", - "\"", - "\"", - "L", - "i", - "s", - "t", - " ", - "e", - "v", - "e", - "r", - "y", - " ", - "f", - "a", - "c", - "t", - " ", - "s", - "t", - "o", - "r", - "e", - "d", - " ", - "i", - "n", - " ", - "t", - "h", - "e", - " ", - "c", - "u", - "r", - "r", - "e", - "n", - "t", - " ", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - "'", - "s", - " ", - "k", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "g", - "r", - "a", - "p", - "h", - ".", - "\"", - "\"", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "o", - "w", - "s", - " ", - "=", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - ".", - "c", - "y", - "p", - "h", - "e", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "M", - "A", - "T", - "C", - "H", - " ", - "(", - "a", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - ")", - "-", - "[", - "r", - "]", - "-", - ">", - "(", - "b", - ":", - "E", - "n", - "t", - "i", - "t", - "y", - " ", - "{", - "s", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "$", - "s", - "e", - "s", - "s", - "}", - ")", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "R", - "E", - "T", - "U", - "R", - "N", - " ", - "a", - ".", - "n", - "a", - "m", - "e", - " ", - "A", - "S", - " ", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - ",", - " ", - "t", - "y", - "p", - "e", - "(", - "r", - ")", - " ", - "A", - "S", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - ",", - " ", - "b", - ".", - "n", - "a", - "m", - "e", - " ", - "A", - "S", - " ", - "o", - "b", - "j", - "e", - "c", - "t", - "\"", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "p", - "a", - "r", - "a", - "m", - "s", - "=", - "{", - "\"", - "s", - "e", - "s", - "s", - "\"", - ":", - " ", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "}", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - " ", - " ", - " ", - " ", - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "r", - "o", - "w", - "s", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "N", - "o", - " ", - "f", - "a", - "c", - "t", - "s", - " ", - "s", - "t", - "o", - "r", - "e", - "d", - " ", - "y", - "e", - "t", - "\"", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "\"", - "\\", - "n", - "\"", - ".", - "j", - "o", - "i", - "n", - "(", - "f", - "\"", - "{", - "r", - "[", - "'", - "s", - "u", - "b", - "j", - "e", - "c", - "t", - "'", - "]", - "}", - " ", - "-", - "[", - "{", - "r", - "[", - "'", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - "'", - "]", - "}", - "]", - "-", - ">", - " ", - "{", - "r", - "[", - "'", - "o", - "b", - "j", - "e", - "c", - "t", - "'", - "]", - "}", - "\"", - " ", - "f", - "o", - "r", - " ", - "r", - " ", - "i", - "n", - " ", - "r", - "o", - "w", - "s", - ")", - "\n", - "\n", - "\n", - "t", - "o", - "o", - "l", - "s", - " ", - "=", - " ", - "[", - "s", - "a", - "v", - "e", - "_", - "f", - "a", - "c", - "t", - ",", - " ", - "q", - "u", - "e", - "r", - "y", - "_", - "f", - "a", - "c", - "t", - "s", - ",", - " ", - "f", - "i", - "n", - "d", - "_", - "r", - "e", - "l", - "a", - "t", - "e", - "d", - ",", - " ", - "l", - "i", - "s", - "t", - "_", - "a", - "l", - "l", - "_", - "f", - "a", - "c", - "t", - "s", - "]", + "import os, re, uuid\n", + "from langchain_core.tools import tool\n", + "\n", + "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", + "\n", + "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "# Regex guards for query_facts (demo safety guard).\n", + "_WRITE_CLAUSE_RE = re.compile(\n", + " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", + " re.IGNORECASE,\n", + ")\n", + "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", + "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", + "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", + "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", + "# In production code, use server-side row-level security instead of client regex.\n", + "_SESSION_SCOPE_RE = re.compile(\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", + " re.IGNORECASE,\n", + ")\n", + "\n", + "\n", + "@tool\n", + "def save_fact(subject: str, relation: str, obj: str) -> str:\n", + " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", + " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " rel_type = relation.upper().replace(\" \", \"_\")\n", + " # Validate rel_type before interpolating into Cypher to prevent injection.\n", + " if not _REL_TYPE_RE.fullmatch(rel_type):\n", + " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", + " client.cypher(\n", + " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", + " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", + " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", + " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", + " )\n", + " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", + "\n", + "\n", + "@tool\n", + "def query_facts(cypher: str) -> str:\n", + " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", + " Must scope reads via either WHERE .session = $sess\n", + " or a node pattern {session: $sess}.\"\"\"\n", + " q = cypher.strip()\n", + " if _WRITE_CLAUSE_RE.search(q):\n", + " return \"Only read-only Cypher is allowed in query_facts.\"\n", + " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", + " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", + " if not _SESSION_SCOPE_RE.search(q):\n", + " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", + " rows = client.cypher(q, params={\"sess\": SESSION})\n", + " return str(rows[:20]) if rows else \"No results\"\n", + "\n", + "\n", + "@tool\n", + "def find_related(entity_name: str, depth: int = 1) -> str:\n", + " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " safe_depth = max(1, min(int(depth), 3))\n", + " rows = client.cypher(\n", + " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", + " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", + " params={\"name\": entity_name, \"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return f\"No related entities found for {entity_name}\"\n", + " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", + "\n", + "@tool\n", + "def list_all_facts() -> str:\n", + " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", + " rows = client.cypher(\n", + " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", + " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", + " params={\"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return \"No facts stored yet\"\n", + " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "S", - "e", - "s", - "s", - "i", - "o", - "n", - ":", - " ", - "{", - "S", - "E", - "S", - "S", - "I", - "O", - "N", - "}", - "\"", - ")", "\n", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "T", - "o", - "o", - "l", - "s", - ":", - "\"", - ",", - " ", - "[", - "t", - ".", - "n", - "a", - "m", - "e", - " ", - "f", - "o", - "r", - " ", - "t", - " ", - "i", - "n", - " ", - "t", - "o", - "o", - "l", - "s", - "]", - ")" + "tools = [save_fact, query_facts, find_related, list_all_facts]\n", + "print(f\"Session: {SESSION}\")\n", + "print(\"Tools:\", [t.name for t in tools])" ] }, { @@ -5850,4 +401,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} From edd0e513f43131351191359dac3a6ad817390dad Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 11:12:31 +0300 Subject: [PATCH 31/86] build(deps): pin coordinode-rs submodule to v0.3.10 Updated from 38a52cf (release-plz merge commit) to bd538f9 (tagged v0.3.10 release commit). The SDK Cargo.toml already requires coordinode >= 0.4.0 for the new schema/text APIs. --- coordinode-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinode-rs b/coordinode-rs index 38a52cf..bd538f9 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 38a52cf505c9a8130ae5845e66b211ae10fffe7f +Subproject commit bd538f97a56c6dd09978010bad0a464fb715fcaf From 588824a3a3ea5fae862be4535b37e346e179f340 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 12:35:05 +0300 Subject: [PATCH 32/86] fix(demo): scope Person MERGE and verify queries to demo_tag, tighten session regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ruff.toml: fix per-file-ignores glob `*.ipynb` → `**/*.ipynb` so rules apply to notebooks in subdirectories (demo/notebooks/) - nb00 seed_data: add `demo_tag: $tag` to Person MERGE identity map so cleanup MATCH and edge MATCH can find Person nodes by tag; scope all verify queries (count loop, rels query, WORKS_AT/USES/BUILDS_ON examples) to `demo_tag: $tag` to avoid mixing rows from other sessions - nb03 langgraph: tighten `_SESSION_SCOPE_RE` second branch to require node pattern context `(...)` — prevents RETURN map literal bypass where `MATCH (n) RETURN {session: $sess} AS s, n` would pass the guard with unscoped `n`; add `re.DOTALL` flag for multi-line node patterns --- demo/notebooks/00_seed_data.ipynb | 28 ++++++++++++++++++------- demo/notebooks/03_langgraph_agent.ipynb | 6 +++--- ruff.toml | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index ff42b99..c3cbc9b 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -1280,7 +1280,11 @@ "]\n", "\n", "for p in people:\n", - " client.cypher(\"MERGE (n:Person {name: $name}) SET n.role = $role, n.field = $field, n.demo = true\", params=p)\n", + " client.cypher(\n", + " \"MERGE (n:Person {name: $name, demo_tag: $tag}) \"\n", + " \"SET n.role = $role, n.field = $field, n.demo = true, n.demo_tag = $tag\",\n", + " params={**p, \"tag\": DEMO_TAG},\n", + " )\n", "\n", "print(f\"Created {len(people)} people\")\n", "\n", @@ -1431,11 +1435,17 @@ "\n", "print(\"Node counts:\")\n", "for label in [\"Person\", \"Company\", \"Technology\"]:\n", - " rows = client.cypher(f\"MATCH (n:{label} {{demo: true}}) RETURN count(n) AS count\")\n", + " rows = client.cypher(\n", + " f\"MATCH (n:{label} {{demo: true, demo_tag: $tag}}) RETURN count(n) AS count\",\n", + " params={\"tag\": DEMO_TAG},\n", + " )\n", " print(f\" {label:15s} {rows[0]['count']}\")\n", "\n", "# Fetch all types and count in Python (avoids aggregation limitations)\n", - "rels = client.cypher(\"MATCH (a {demo: true})-[r]->(b) RETURN type(r) AS rel\")\n", + "rels = client.cypher(\n", + " \"MATCH (a {demo: true, demo_tag: $tag})-[r]->(b {demo: true, demo_tag: $tag}) RETURN type(r) AS rel\",\n", + " params={\"tag\": DEMO_TAG},\n", + ")\n", "counts = Counter(r[\"rel\"] for r in rels)\n", "print(\"\\nRelationship counts:\")\n", "for rel, cnt in sorted(counts.items(), key=lambda x: -x[1]):\n", @@ -1451,23 +1461,25 @@ "source": [ "print(\"=== Who works at Synthex? ===\")\n", "rows = client.cypher(\n", - " \"MATCH (p:Person)-[:WORKS_AT]->(c:Company {name: $co}) RETURN p.name AS name, p.role AS role\",\n", - " params={\"co\": \"Synthex\"},\n", + " \"MATCH (p:Person {demo_tag: $tag})-[:WORKS_AT]->(c:Company {name: $co, demo_tag: $tag}) \"\n", + " \"RETURN p.name AS name, p.role AS role\",\n", + " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n", ")\n", "for r in rows:\n", " print(f\" {r['name']} — {r['role']}\")\n", "\n", "print(\"\\n=== What does Synthex use? ===\")\n", "rows = client.cypher(\n", - " \"MATCH (c:Company {name: $co})-[:USES]->(t:Technology) RETURN t.name AS name\", params={\"co\": \"Synthex\"}\n", + " \"MATCH (c:Company {name: $co, demo_tag: $tag})-[:USES]->(t:Technology {demo_tag: $tag}) RETURN t.name AS name\",\n", + " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG}\n", ")\n", "for r in rows:\n", " print(f\" {r['name']}\")\n", "\n", "print(\"\\n=== GraphRAG dependency chain ===\")\n", "rows = client.cypher(\n", - " \"MATCH (t:Technology {name: $tech})-[:BUILDS_ON*1..3]->(dep) RETURN dep.name AS dependency\",\n", - " params={\"tech\": \"GraphRAG\"},\n", + " \"MATCH (t:Technology {name: $tech, demo_tag: $tag})-[:BUILDS_ON*1..3]->(dep:Technology {demo_tag: $tag}) RETURN dep.name AS dependency\",\n", + " params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n", ")\n", "for r in rows:\n", " print(f\" → {r['dependency']}\")\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index d551141..fb902d4 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -144,7 +144,7 @@ "# Regex guards for query_facts (demo safety guard).\n", "_WRITE_CLAUSE_RE = re.compile(\n", " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", - " re.IGNORECASE,\n", + " re.IGNORECASE | re.DOTALL,\n", ")\n", "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", @@ -152,8 +152,8 @@ "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", "# In production code, use server-side row-level security instead of client regex.\n", "_SESSION_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}\",\n", - " re.IGNORECASE,\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n", + " re.IGNORECASE | re.DOTALL,\n", ")\n", "\n", "\n", diff --git a/ruff.toml b/ruff.toml index 0875667..05039df 100644 --- a/ruff.toml +++ b/ruff.toml @@ -21,7 +21,7 @@ ignore = [ [lint.per-file-ignores] # Jupyter notebooks: multi-import lines, out-of-order imports after subprocess # install cells, and unsorted imports are normal in demo notebook code. -"*.ipynb" = ["E401", "E402", "I001"] +"**/*.ipynb" = ["E401", "E402", "I001"] [lint.isort] known-first-party = ["coordinode", "langchain_coordinode", "llama_index_coordinode"] From 9e9c343a27ccc55b1cbb2accfb9811b66721cf2c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 13:05:16 +0300 Subject: [PATCH 33/86] fix(notebooks): repair source format, update warning text, split regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nb00 cell 0: convert first markdown cell from char-by-char to line-per-string array (notebook diff noise) - nb00: update warning text to reflect demo_tag scoping — MERGE and cleanup are now scoped by demo_tag, not by name only - nb00: clarify that the LocalClient fallback requires coordinode-embedded to be pre-installed (the install cell only provisions it in Colab) - nb00–03: add comment explaining why SHA256 pinning for rustup-init is intentionally omitted (HTTPS/TLS + temp-file execution is the rustup team's recommended trust model; no stable per-script checksum exists) - nb03: split _SESSION_SCOPE_RE into _SESSION_WHERE_SCOPE_RE and _SESSION_NODE_SCOPE_RE to reduce regex complexity (SonarCloud: 24→~12) - tests: narrow _fts xfail from any grpc.RpcError to UNIMPLEMENTED only, so INVALID_ARGUMENT or other gRPC errors surface as real failures --- demo/notebooks/00_seed_data.ipynb | 1207 +---------------- .../01_llama_index_property_graph.ipynb | 64 +- demo/notebooks/02_langchain_graph_chain.ipynb | 61 +- demo/notebooks/03_langgraph_agent.ipynb | 149 +- tests/integration/test_sdk.py | 39 +- 5 files changed, 54 insertions(+), 1466 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index c3cbc9b..630b404 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -5,1130 +5,31 @@ "id": "a1b2c3d4-0000-0000-0000-000000000001", "metadata": {}, "source": [ - "#", - " ", - "S", - "e", - "e", - "d", - " ", - "D", - "e", - "m", - "o", - " ", - "D", - "a", - "t", - "a", + "# Seed Demo Data\n", "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb)\n", "\n", - "[", - "!", - "[", - "O", - "p", - "e", - "n", - " ", - "i", - "n", - " ", - "C", - "o", - "l", - "a", - "b", - "]", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "c", - "o", - "l", - "a", - "b", - ".", - "r", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - ".", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "m", - "/", - "a", - "s", - "s", - "e", - "t", - "s", - "/", - "c", - "o", - "l", - "a", - "b", - "-", - "b", - "a", - "d", - "g", - "e", - ".", - "s", - "v", - "g", - ")", - "]", - "(", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "c", - "o", - "l", - "a", - "b", - ".", - "r", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - ".", - "g", - "o", - "o", - "g", - "l", - "e", - ".", - "c", - "o", - "m", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - "/", - "b", - "l", - "o", - "b", - "/", - "m", - "a", - "i", - "n", - "/", - "d", - "e", - "m", - "o", - "/", - "n", - "o", - "t", - "e", - "b", - "o", - "o", - "k", - "s", - "/", - "0", - "0", - "_", - "s", - "e", - "e", - "d", - "_", - "d", - "a", - "t", - "a", - ".", - "i", - "p", - "y", - "n", - "b", - ")", + "Populates CoordiNode with a **tech industry knowledge graph** you can explore\n", + "in notebooks 01–03.\n", "\n", + "**Graph contents:**\n", + "- 10 people (engineers, researchers, founders)\n", + "- 6 companies\n", + "- 8 technologies / research areas\n", + "- ~35 relationships (WORKS_AT, FOUNDED, KNOWS, RESEARCHES, INVENTED, ACQUIRED, USES, …)\n", "\n", - "P", - "o", - "p", - "u", - "l", - "a", - "t", - "e", - "s", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "w", - "i", - "t", - "h", - " ", - "a", - " ", - "*", - "*", - "t", - "e", - "c", - "h", - " ", - "i", - "n", - "d", - "u", - "s", - "t", - "r", - "y", - " ", - "k", - "n", - "o", - "w", - "l", - "e", - "d", - "g", - "e", - " ", - "g", - "r", - "a", - "p", - "h", - "*", - "*", - " ", - "y", - "o", - "u", - " ", - "c", - "a", - "n", - " ", - "e", - "x", - "p", - "l", - "o", - "r", - "e", + "All nodes carry a `demo=true` property and a `demo_tag` equal to the `DEMO_TAG` variable\n", + "set in the seed cell. MERGE operations and cleanup are scoped to that tag, so only nodes\n", + "with the matching `demo_tag` are written or removed.\n", "\n", - "i", - "n", - " ", - "n", - "o", - "t", - "e", - "b", - "o", - "o", - "k", - "s", - " ", - "0", - "1", - "–", - "0", - "3", - ".", + "**Environments:**\n", + "- **Google Colab** — uses `coordinode-embedded` (in-process Rust engine, no server needed). First run compiles from source (~5 min); subsequent runs use the pip cache.\n", + "- **Local / Docker Compose** — connects to a running CoordiNode server via gRPC.\n", "\n", - "\n", - "*", - "*", - "G", - "r", - "a", - "p", - "h", - " ", - "c", - "o", - "n", - "t", - "e", - "n", - "t", - "s", - ":", - "*", - "*", - "\n", - "-", - " ", - "1", - "0", - " ", - "p", - "e", - "o", - "p", - "l", - "e", - " ", - "(", - "e", - "n", - "g", - "i", - "n", - "e", - "e", - "r", - "s", - ",", - " ", - "r", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - "e", - "r", - "s", - ",", - " ", - "f", - "o", - "u", - "n", - "d", - "e", - "r", - "s", - ")", - "\n", - "-", - " ", - "6", - " ", - "c", - "o", - "m", - "p", - "a", - "n", - "i", - "e", - "s", - "\n", - "-", - " ", - "8", - " ", - "t", - "e", - "c", - "h", - "n", - "o", - "l", - "o", - "g", - "i", - "e", - "s", - " ", - "/", - " ", - "r", - "e", - "s", - "e", - "a", - "r", - "c", - "h", - " ", - "a", - "r", - "e", - "a", - "s", - "\n", - "-", - " ", - "~", - "3", - "5", - " ", - "r", - "e", - "l", - "a", - "t", - "i", - "o", - "n", - "s", - "h", - "i", - "p", - "s", - " ", - "(", - "W", - "O", - "R", - "K", - "S", - "_", - "A", - "T", - ",", - " ", - "F", - "O", - "U", - "N", - "D", - "E", - "D", - ",", - " ", - "K", - "N", - "O", - "W", - "S", - ",", - " ", - "R", - "E", - "S", - "E", - "A", - "R", - "C", - "H", - "E", - "S", - ",", - " ", - "I", - "N", - "V", - "E", - "N", - "T", - "E", - "D", - ",", - " ", - "A", - "C", - "Q", - "U", - "I", - "R", - "E", - "D", - ",", - " ", - "U", - "S", - "E", - "S", - ",", - " ", - "…", - ")", - "\n", - "\n", - "A", - "l", - "l", - " ", - "n", - "o", - "d", - "e", - "s", - " ", - "c", - "a", - "r", - "r", - "y", - " ", - "a", - " ", - "`", - "d", - "e", - "m", - "o", - "=", - "t", - "r", - "u", - "e", - "`", - " ", - "p", - "r", - "o", - "p", - "e", - "r", - "t", - "y", - " ", - "s", - "o", - " ", - "t", - "h", - "e", - "y", - " ", - "c", - "a", - "n", - " ", - "b", - "e", - " ", - "b", - "u", - "l", - "k", - "-", - "d", - "e", - "l", - "e", - "t", - "e", - "d", - " ", - "w", - "i", - "t", - "h", - "o", - "u", - "t", - "\n", - "t", - "o", - "u", - "c", - "h", - "i", - "n", - "g", - " ", - "o", - "t", - "h", - "e", - "r", - " ", - "d", - "a", - "t", - "a", - ".", - "\n", - "\n", - "*", - "*", - "E", - "n", - "v", - "i", - "r", - "o", - "n", - "m", - "e", - "n", - "t", - "s", - ":", - "*", - "*", - "\n", - "-", - " ", - "*", - "*", - "G", - "o", - "o", - "g", - "l", - "e", - " ", - "C", - "o", - "l", - "a", - "b", - "*", - "*", - " ", - "—", - " ", - "u", - "s", - "e", - "s", - " ", - "`", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "`", - " ", - "(", - "i", - "n", - "-", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - " ", - "R", - "u", - "s", - "t", - " ", - "e", - "n", - "g", - "i", - "n", - "e", - ",", - " ", - "n", - "o", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "n", - "e", - "e", - "d", - "e", - "d", - ")", - ".", - " ", - "F", - "i", - "r", - "s", - "t", - " ", - "r", - "u", - "n", - " ", - "c", - "o", - "m", - "p", - "i", - "l", - "e", - "s", - " ", - "f", - "r", - "o", - "m", - " ", - "s", - "o", - "u", - "r", - "c", - "e", - " ", - "(", - "~", - "5", - " ", - "m", - "i", - "n", - ")", - ";", - " ", - "s", - "u", - "b", - "s", - "e", - "q", - "u", - "e", - "n", - "t", - " ", - "r", - "u", - "n", - "s", - " ", - "u", - "s", - "e", - " ", - "t", - "h", - "e", - " ", - "p", - "i", - "p", - " ", - "c", - "a", - "c", - "h", - "e", - ".", - "\n", - "-", - " ", - "*", - "*", - "L", - "o", - "c", - "a", - "l", - " ", - "/", - " ", - "D", - "o", - "c", - "k", - "e", - "r", - " ", - "C", - "o", - "m", - "p", - "o", - "s", - "e", - "*", - "*", - " ", - "—", - " ", - "c", - "o", - "n", - "n", - "e", - "c", - "t", - "s", - " ", - "t", - "o", - " ", - "a", - " ", - "r", - "u", - "n", - "n", - "i", - "n", - "g", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "v", - "i", - "a", - " ", - "g", - "R", - "P", - "C", - ".", - "\n", - "\n", - ">", - " ", - "*", - "*", - "⚠", - "️", - " ", - "N", - "o", - "t", - "e", - " ", - "f", - "o", - "r", - " ", - "r", - "e", - "a", - "l", - "-", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "u", - "s", - "e", - ":", - "*", - "*", - " ", - "M", - "E", - "R", - "G", - "E", - " ", - "o", - "p", - "e", - "r", - "a", - "t", - "i", - "o", - "n", - "s", - " ", - "m", - "a", - "t", - "c", - "h", - " ", - "n", - "o", - "d", - "e", - "s", - " ", - "b", - "y", - " ", - "n", - "a", - "m", - "e", - " ", - "(", - "e", - ".", - "g", - ".", - " ", - "`", - "\"", - "G", - "o", - "o", - "g", - "l", - "e", - "\"", - "`", - ")", - ".", - " ", - "R", - "u", - "n", - " ", - "t", - "h", - "i", - "s", - " ", - "n", - "o", - "t", - "e", - "b", - "o", - "o", - "k", - " ", - "a", - "g", - "a", - "i", - "n", - "s", - "t", - " ", - "a", - " ", - "f", - "r", - "e", - "s", - "h", - "/", - "e", - "m", - "p", - "t", - "y", - " ", - "d", - "a", - "t", - "a", - "b", - "a", - "s", - "e", - " ", - "o", - "r", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "m", - "o", - "d", - "e", - " ", - "t", - "o", - " ", - "a", - "v", - "o", - "i", - "d", - " ", - "a", - "c", - "c", - "i", - "d", - "e", - "n", - "t", - "a", - "l", - "l", - "y", - " ", - "t", - "a", - "g", - "g", - "i", - "n", - "g", - " ", - "a", - "n", - "d", - " ", - "t", - "h", - "e", - "n", - " ", - "d", - "e", - "l", - "e", - "t", - "i", - "n", - "g", - " ", - "p", - "r", - "e", - "-", - "e", - "x", - "i", - "s", - "t", - "i", - "n", - "g", - " ", - "n", - "o", - "d", - "e", - "s", - " ", - "d", - "u", - "r", - "i", - "n", - "g", - " ", - "c", - "l", - "e", - "a", - "n", - "u", - "p", - "." + "> **⚠️ Note for real-server use:** All writes and the cleanup step are scoped to `demo_tag`.\n", + "> Collisions can occur if multiple runs reuse the same `demo_tag` value or if `demo_tag` is\n", + "> empty. Run against a fresh/empty database or choose a unique `demo_tag` to avoid affecting\n", + "> unrelated nodes." ] }, { @@ -1145,79 +46,13 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Install coordinode-embedded only when no gRPC server is available (Colab path).\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " # Add cargo to PATH so maturin/pip can find it.\n", - " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", - " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " ],\n", - " check=True,\n", - " timeout=600,\n", - " )\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"Ready\")" - ] + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", "id": "a1b2c3d4-0000-0000-0000-000000000004", "metadata": {}, - "source": [ - "## Connect to CoordiNode\n", - "\n", - "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", - "- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n", - "- **Local without server**: falls back to embedded engine automatically." - ] + "source": "## Connect to CoordiNode\n\n- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n- **Local with server**: connects to an existing CoordiNode on port 7080 (set `COORDINODE_ADDR` to override).\n- **Local without server**: falls back to `coordinode-embedded` if already installed (see [coordinode-embedded](https://github.com/structured-world/coordinode-python/tree/main/coordinode-embedded)); otherwise shows a `RuntimeError` with install instructions." }, { "cell_type": "code", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index c784691..6cbc416 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,69 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Install coordinode-embedded in Colab only (requires Rust build).\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " # Add cargo to PATH so maturin/pip can find it.\n", - " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", - " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " ],\n", - " check=True,\n", - " timeout=600,\n", - " )\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"llama-index-graph-stores-coordinode\",\n", - " \"llama-index-core\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 758c5c0..c025969 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,66 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Install coordinode-embedded in Colab only (requires Rust build).\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " # Add cargo to PATH so maturin/pip can find it.\n", - " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", - " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " ],\n", - " check=True,\n", - " timeout=600,\n", - " )\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"langchain\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - ")\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index fb902d4..36621df 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,66 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Install coordinode-embedded in Colab only (requires Rust build).\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " # Add cargo to PATH so maturin/pip can find it.\n", - " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", - " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n", - " ],\n", - " check=True,\n", - " timeout=600,\n", - " )\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"langchain-coordinode\",\n", - " \"langchain-community\",\n", - " \"langchain-openai\",\n", - " \"langgraph\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - ")\n", - "\n", - "print(\"SDK installed\")" - ] + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -134,93 +75,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": [ - "import os, re, uuid\n", - "from langchain_core.tools import tool\n", - "\n", - "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", - "\n", - "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", - "# Regex guards for query_facts (demo safety guard).\n", - "_WRITE_CLAUSE_RE = re.compile(\n", - " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", - " re.IGNORECASE | re.DOTALL,\n", - ")\n", - "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", - "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", - "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", - "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", - "# In production code, use server-side row-level security instead of client regex.\n", - "_SESSION_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess|\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n", - " re.IGNORECASE | re.DOTALL,\n", - ")\n", - "\n", - "\n", - "@tool\n", - "def save_fact(subject: str, relation: str, obj: str) -> str:\n", - " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", - " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", - " rel_type = relation.upper().replace(\" \", \"_\")\n", - " # Validate rel_type before interpolating into Cypher to prevent injection.\n", - " if not _REL_TYPE_RE.fullmatch(rel_type):\n", - " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", - " client.cypher(\n", - " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", - " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", - " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", - " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", - " )\n", - " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", - "\n", - "\n", - "@tool\n", - "def query_facts(cypher: str) -> str:\n", - " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", - " Must scope reads via either WHERE .session = $sess\n", - " or a node pattern {session: $sess}.\"\"\"\n", - " q = cypher.strip()\n", - " if _WRITE_CLAUSE_RE.search(q):\n", - " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", - " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", - " if not _SESSION_SCOPE_RE.search(q):\n", - " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", - " rows = client.cypher(q, params={\"sess\": SESSION})\n", - " return str(rows[:20]) if rows else \"No results\"\n", - "\n", - "\n", - "@tool\n", - "def find_related(entity_name: str, depth: int = 1) -> str:\n", - " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", - " safe_depth = max(1, min(int(depth), 3))\n", - " rows = client.cypher(\n", - " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", - " \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n", - " params={\"name\": entity_name, \"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return f\"No related entities found for {entity_name}\"\n", - " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", - "\n", - "\n", - "@tool\n", - "def list_all_facts() -> str:\n", - " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", - " rows = client.cypher(\n", - " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", - " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", - " params={\"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return \"No facts stored yet\"\n", - " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", - "\n", - "\n", - "tools = [save_fact, query_facts, find_related, list_all_facts]\n", - "print(f\"Session: {SESSION}\")\n", - "print(\"Tools:\", [t.name for t in tools])" - ] + "source": "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" }, { "cell_type": "markdown", diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 6d4e505..8adb2b2 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -7,6 +7,7 @@ from __future__ import annotations +import functools import os import uuid @@ -550,6 +551,7 @@ def test_vector_search_returns_results(client): # ── Full-text search ────────────────────────────────────────────────────────── + # FTS tests require a CoordiNode server with TextService implemented (>=0.3.8). # They are marked xfail so the suite stays green against older servers; once # upgraded, the tests turn into expected passes automatically. @@ -561,16 +563,33 @@ def test_vector_search_returns_results(client): # and hybrid_text_vector_search() work on schema-free graphs too. These tests # exercise the common schema-free path; calling create_label() here would test a # different (schema-strict) code path and is covered by test_create_label_*. -_fts = pytest.mark.xfail( - reason="TextService requires CoordiNode >=0.3.8 with FTS support", - strict=False, - # AssertionError is the actual observed failure mode on servers without FTS: - # text_search() returns [] (empty result set), triggering `assert len(...) >= 1`. - # grpc.RpcError covers servers that raise UNIMPLEMENTED. Both are expected - # until the server is upgraded; removing AssertionError would cause those tests - # to error-out (unexpected failure) rather than xfail. - raises=(AssertionError, grpc.RpcError), -) +def _fts(fn): + """Mark an FTS test as expected-failure on servers without TextService. + + Two expected failure modes: + - ``AssertionError``: server returns an empty result set (no FTS index), caught + by the marker directly so pytest reports it as xfail. + - ``grpc.StatusCode.UNIMPLEMENTED``: TextService RPC does not exist yet; we call + ``pytest.xfail()`` explicitly so only that specific status code is silenced. + Any other ``grpc.RpcError`` (e.g. INVALID_ARGUMENT from a malformed request) + propagates as a real test failure, preventing regressions from being masked. + """ + + @pytest.mark.xfail( + reason="TextService requires CoordiNode >=0.3.8 with FTS support", + strict=False, + raises=AssertionError, + ) + @functools.wraps(fn) + def wrapper(*args, **kwargs): + try: + return fn(*args, **kwargs) + except grpc.RpcError as exc: + if exc.code() == grpc.StatusCode.UNIMPLEMENTED: + pytest.xfail(f"TextService not implemented: {exc.details()}") + raise # other gRPC errors (e.g. INVALID_ARGUMENT) surface as failures + + return wrapper @_fts From f61e7dd6207cbffa02a84cea2284473e4686391a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 15:01:43 +0300 Subject: [PATCH 34/86] fix(notebooks,tests): clarify embedded isolation, narrow FTS xfail scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nb00: update intro and final message to explain that LocalClient(":memory:") is process-local — notebooks 01-03 cannot see seeded data in embedded mode; add note that cross-notebook sharing requires a shared CoordiNode server - tests: remove raises=AssertionError from _fts xfail marker; instead call pytest.xfail() inline in the two hit-bearing tests when results are empty, so all other AssertionErrors (wrong types, malformed scores) surface as real failures on FTS-capable servers --- demo/notebooks/00_seed_data.ipynb | 38 ++++++------------------------- tests/integration/test_sdk.py | 24 +++++++++++-------- 2 files changed, 21 insertions(+), 41 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 630b404..1b97133 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -9,8 +9,12 @@ "\n", "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/structured-world/coordinode-python/blob/main/demo/notebooks/00_seed_data.ipynb)\n", "\n", - "Populates CoordiNode with a **tech industry knowledge graph** you can explore\n", - "in notebooks 01–03.\n", + "Populates CoordiNode with a **tech industry knowledge graph**.\n", + "\n", + "> **Note:** When using `coordinode-embedded` (`LocalClient(\":memory:\")`), the seeded data\n", + "> lives only inside this notebook process — notebooks 01–03 will start with an empty graph.\n", + "> To share the graph across notebooks, point all of them at the same running CoordiNode\n", + "> server via `COORDINODE_ADDR`.\n", "\n", "**Graph contents:**\n", "- 10 people (engineers, researchers, founders)\n", @@ -293,35 +297,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000014", "metadata": {}, "outputs": [], - "source": [ - "print(\"=== Who works at Synthex? ===\")\n", - "rows = client.cypher(\n", - " \"MATCH (p:Person {demo_tag: $tag})-[:WORKS_AT]->(c:Company {name: $co, demo_tag: $tag}) \"\n", - " \"RETURN p.name AS name, p.role AS role\",\n", - " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n", - ")\n", - "for r in rows:\n", - " print(f\" {r['name']} — {r['role']}\")\n", - "\n", - "print(\"\\n=== What does Synthex use? ===\")\n", - "rows = client.cypher(\n", - " \"MATCH (c:Company {name: $co, demo_tag: $tag})-[:USES]->(t:Technology {demo_tag: $tag}) RETURN t.name AS name\",\n", - " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG}\n", - ")\n", - "for r in rows:\n", - " print(f\" {r['name']}\")\n", - "\n", - "print(\"\\n=== GraphRAG dependency chain ===\")\n", - "rows = client.cypher(\n", - " \"MATCH (t:Technology {name: $tech, demo_tag: $tag})-[:BUILDS_ON*1..3]->(dep:Technology {demo_tag: $tag}) RETURN dep.name AS dependency\",\n", - " params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n", - ")\n", - "for r in rows:\n", - " print(f\" → {r['dependency']}\")\n", - "\n", - "print(\"\\n✓ Demo data ready — open notebooks 01, 02, 03 to explore!\")\n", - "client.close()" - ] + "source": "print(\"=== Who works at Synthex? ===\")\nrows = client.cypher(\n \"MATCH (p:Person {demo_tag: $tag})-[:WORKS_AT]->(c:Company {name: $co, demo_tag: $tag}) \"\n \"RETURN p.name AS name, p.role AS role\",\n params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n)\nfor r in rows:\n print(f\" {r['name']} — {r['role']}\")\n\nprint(\"\\n=== What does Synthex use? ===\")\nrows = client.cypher(\n \"MATCH (c:Company {name: $co, demo_tag: $tag})-[:USES]->(t:Technology {demo_tag: $tag}) RETURN t.name AS name\",\n params={\"co\": \"Synthex\", \"tag\": DEMO_TAG}\n)\nfor r in rows:\n print(f\" {r['name']}\")\n\nprint(\"\\n=== GraphRAG dependency chain ===\")\nrows = client.cypher(\n \"MATCH (t:Technology {name: $tech, demo_tag: $tag})-[:BUILDS_ON*1..3]->(dep:Technology {demo_tag: $tag}) RETURN dep.name AS dependency\",\n params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n)\nfor r in rows:\n print(f\" → {r['dependency']}\")\n\nprint(\"\\n✓ Demo data seeded.\")\nprint(\"To query it from notebooks 01–03, connect them to the same CoordiNode server (COORDINODE_ADDR).\")\nclient.close()" } ], "metadata": { diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 8adb2b2..0dc928e 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -566,19 +566,21 @@ def test_vector_search_returns_results(client): def _fts(fn): """Mark an FTS test as expected-failure on servers without TextService. - Two expected failure modes: - - ``AssertionError``: server returns an empty result set (no FTS index), caught - by the marker directly so pytest reports it as xfail. - - ``grpc.StatusCode.UNIMPLEMENTED``: TextService RPC does not exist yet; we call - ``pytest.xfail()`` explicitly so only that specific status code is silenced. - Any other ``grpc.RpcError`` (e.g. INVALID_ARGUMENT from a malformed request) - propagates as a real test failure, preventing regressions from being masked. + Expected failure modes handled explicitly — NOT via ``raises=`` on the marker: + - ``grpc.StatusCode.UNIMPLEMENTED``: TextService RPC does not exist; caught in + the wrapper and converted to ``pytest.xfail()`` so only this status is silenced. + - Empty result set: tests that require at least one hit call ``pytest.xfail()`` + inline (``if not results: pytest.xfail(...)``), keeping the xfail scoped to + that specific condition. + + Any ``AssertionError`` that is NOT the "no results" case (e.g. wrong return type, + malformed score) propagates as a real test failure so regressions on FTS-capable + servers are visible in CI. """ @pytest.mark.xfail( reason="TextService requires CoordiNode >=0.3.8 with FTS support", strict=False, - raises=AssertionError, ) @functools.wraps(fn) def wrapper(*args, **kwargs): @@ -603,7 +605,8 @@ def test_text_search_returns_results(client): try: results = client.text_search("FtsTest", "machine learning", limit=5) assert isinstance(results, list) - assert len(results) >= 1, "text_search returned no results" + if not results: + pytest.xfail("text_search returned no results — FTS index not available on this server") r = results[0] assert isinstance(r, TextResult) assert isinstance(r.node_id, int) @@ -656,7 +659,8 @@ def test_hybrid_text_vector_search_returns_results(client): limit=5, ) assert isinstance(results, list) - assert len(results) >= 1, "hybrid_text_vector_search returned no results" + if not results: + pytest.xfail("hybrid_text_vector_search returned no results — FTS index not available on this server") r = results[0] assert isinstance(r, HybridResult) assert isinstance(r.node_id, int) From a4579b9e561bc6f9897a55316724d8633383c5ab Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 15:47:13 +0300 Subject: [PATCH 35/86] fix(tests): remove broad xfail from _fts, namespace FTS labels per run Remove the unconditional @pytest.mark.xfail decorator from the _fts wrapper. Without raises=, any AssertionError inside the wrapped test was silently reported as XFAIL, hiding client-side regressions on FTS-capable servers. Only explicit pytest.xfail() calls remain: one in the except handler for grpc.StatusCode.UNIMPLEMENTED, and inline guards for empty result sets. Namespace FtsTest and FtsHybridTest labels with a uid() suffix so each run creates its own label class. Stale nodes from previous runs can no longer satisfy the search query and produce false positives on a reused instance. Add assert any(r.node_id == seed_id for r in results) to verify the seeded node is actually returned, not some unrelated stale match. --- tests/integration/test_sdk.py | 52 +++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 0dc928e..211f7bd 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -564,24 +564,18 @@ def test_vector_search_returns_results(client): # exercise the common schema-free path; calling create_label() here would test a # different (schema-strict) code path and is covered by test_create_label_*. def _fts(fn): - """Mark an FTS test as expected-failure on servers without TextService. - - Expected failure modes handled explicitly — NOT via ``raises=`` on the marker: - - ``grpc.StatusCode.UNIMPLEMENTED``: TextService RPC does not exist; caught in - the wrapper and converted to ``pytest.xfail()`` so only this status is silenced. - - Empty result set: tests that require at least one hit call ``pytest.xfail()`` - inline (``if not results: pytest.xfail(...)``), keeping the xfail scoped to - that specific condition. - - Any ``AssertionError`` that is NOT the "no results" case (e.g. wrong return type, - malformed score) propagates as a real test failure so regressions on FTS-capable - servers are visible in CI. + """Wrap an FTS test to handle servers without TextService. + + Explicit xfail conditions: + - ``grpc.StatusCode.UNIMPLEMENTED``: TextService RPC does not exist → ``pytest.xfail()``. + - Empty result set: hit-requiring tests call ``pytest.xfail()`` inline. + + All other exceptions (wrong return type, malformed score, unexpected gRPC errors) + propagate as real failures so regressions on FTS-capable servers are visible in CI. + No ``pytest.mark.xfail`` decorator is applied — that would silently swallow any + AssertionError and hide client-side regressions. """ - @pytest.mark.xfail( - reason="TextService requires CoordiNode >=0.3.8 with FTS support", - strict=False, - ) @functools.wraps(fn) def wrapper(*args, **kwargs): try: @@ -597,16 +591,21 @@ def wrapper(*args, **kwargs): @_fts def test_text_search_returns_results(client): """text_search() finds nodes whose text property matches the query.""" + label = f"FtsTest_{uid()}" tag = uid() - client.cypher( - "CREATE (n:FtsTest {tag: $tag, body: 'machine learning and neural networks'})", + rows = client.cypher( + f"CREATE (n:{label} {{tag: $tag, body: 'machine learning and neural networks'}}) RETURN n AS node_id", params={"tag": tag}, ) + seed_id = rows[0]["node_id"] try: - results = client.text_search("FtsTest", "machine learning", limit=5) + results = client.text_search(label, "machine learning", limit=5) assert isinstance(results, list) if not results: pytest.xfail("text_search returned no results — FTS index not available on this server") + assert any(r.node_id == seed_id for r in results), ( + f"seeded node {seed_id} not found in text_search results: {results}" + ) r = results[0] assert isinstance(r, TextResult) assert isinstance(r.node_id, int) @@ -614,7 +613,7 @@ def test_text_search_returns_results(client): assert r.score > 0 assert isinstance(r.snippet, str) finally: - client.cypher("MATCH (n:FtsTest {tag: $tag}) DELETE n", params={"tag": tag}) + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @_fts @@ -645,15 +644,17 @@ def test_text_search_fuzzy(client): @_fts def test_hybrid_text_vector_search_returns_results(client): """hybrid_text_vector_search() returns HybridResult list with RRF scores.""" + label = f"FtsHybridTest_{uid()}" tag = uid() vec = [float(i) / 16 for i in range(16)] - client.cypher( - "CREATE (n:FtsHybridTest {tag: $tag, body: 'graph neural network embedding', embedding: $vec})", + rows = client.cypher( + f"CREATE (n:{label} {{tag: $tag, body: 'graph neural network embedding', embedding: $vec}}) RETURN n AS node_id", params={"tag": tag, "vec": vec}, ) + seed_id = rows[0]["node_id"] try: results = client.hybrid_text_vector_search( - "FtsHybridTest", + label, "graph neural", vec, limit=5, @@ -661,10 +662,13 @@ def test_hybrid_text_vector_search_returns_results(client): assert isinstance(results, list) if not results: pytest.xfail("hybrid_text_vector_search returned no results — FTS index not available on this server") + assert any(r.node_id == seed_id for r in results), ( + f"seeded node {seed_id} not found in hybrid_text_vector_search results: {results}" + ) r = results[0] assert isinstance(r, HybridResult) assert isinstance(r.node_id, int) assert isinstance(r.score, float) assert r.score > 0 finally: - client.cypher("MATCH (n:FtsHybridTest {tag: $tag}) DETACH DELETE n", params={"tag": tag}) + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DETACH DELETE n", params={"tag": tag}) From 3a810449549bf0577e3272236fd7290691df07a0 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 16:10:24 +0300 Subject: [PATCH 36/86] fix(tests,langchain,demo): namespace FtsFuzzyTest, pass params to cypher, clarify README Namespace FtsFuzzyTest label with uid() suffix for isolation parity with the other two FTS tests (FtsTest_ and FtsHybridTest_ already namespaced). Pass explicit empty params dict {} to the cypher() call in refresh_schema() so injected clients without a default params argument don't raise TypeError. Explicitly name demo/docker-compose.yml in demo/README.md so the file is unambiguous in PR review context (only README was new in this PR; the compose file was pre-existing and not visible in the diff). Also add missing "cd demo/" to the OpenAI code block to ensure the correct compose file is targeted. --- demo/README.md | 3 +++ langchain-coordinode/langchain_coordinode/graph.py | 3 ++- tests/integration/test_sdk.py | 7 ++++--- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/demo/README.md b/demo/README.md index 1aa7af5..7b484b6 100644 --- a/demo/README.md +++ b/demo/README.md @@ -16,6 +16,8 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. ## Run locally (Docker Compose) +`demo/docker-compose.yml` provides a CoordiNode + Jupyter Lab stack: + ```bash cd demo/ docker compose up -d --build @@ -34,5 +36,6 @@ Notebooks 02 and 03 have optional sections that use `OPENAI_API_KEY`. They auto-skip when the key is absent — all core features work without LLM. ```bash +cd demo/ OPENAI_API_KEY=sk-... docker compose up -d ``` diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index d43f268..d98c592 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -153,7 +153,8 @@ def refresh_schema(self) -> None: # Note: can simplify to labels(a)[0] once subscript-on-function support lands in the # published Docker image (tracked in G010 / GAPS.md). rows = self._client.cypher( - "MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels" + "MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels", + {}, ) if rows: triples: set[tuple[str, str, str]] = set() diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 211f7bd..f7a7445 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -626,19 +626,20 @@ def test_text_search_empty_for_unindexed_label(client): @_fts def test_text_search_fuzzy(client): """text_search() with fuzzy=True matches approximate terms.""" + label = f"FtsFuzzyTest_{uid()}" tag = uid() client.cypher( - "CREATE (n:FtsFuzzyTest {tag: $tag, body: 'coordinode graph database'})", + f"CREATE (n:{label} {{tag: $tag, body: 'coordinode graph database'}})", params={"tag": tag}, ) try: # "coordinode" with a typo — fuzzy should still match - results = client.text_search("FtsFuzzyTest", "coordinod", fuzzy=True, limit=5) + results = client.text_search(label, "coordinod", fuzzy=True, limit=5) assert isinstance(results, list) # May return 0 results if fuzzy is not yet supported or index is cold; # just verify the call does not raise. finally: - client.cypher("MATCH (n:FtsFuzzyTest {tag: $tag}) DELETE n", params={"tag": tag}) + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @_fts From d1966b6742c7bdac20aa1be76f4698aeb505ec7f Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 16:29:04 +0300 Subject: [PATCH 37/86] build(deps): bump coordinode to v0.3.11, update proto submodule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coordinode-rs submodule: v0.3.10 (bd538f9) → v0.3.11 (0ff4b47) Bug fixes: MERGE on existing node with unique constraint, WAL crash durability, cluster join protocol - proto submodule: 97c11c6 → 0428726 New RPCs: JoinNode, JoinProgress (cluster join); read routing and consistency fields in ExecuteCypher; DecommissionNode in cluster admin - docker-compose.yml: coordinode:0.3.10 → coordinode:0.3.11 --- coordinode-rs | 2 +- docker-compose.yml | 2 +- proto | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index bd538f9..0ff4b47 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit bd538f97a56c6dd09978010bad0a464fb715fcaf +Subproject commit 0ff4b47693ab9b99599cb9bdb8b7532f03aa7f6e diff --git a/docker-compose.yml b/docker-compose.yml index 2c01e75..f3f8f63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ version: "3.9" services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.10 + image: ghcr.io/structured-world/coordinode:0.3.11 container_name: coordinode ports: - "7080:7080" # gRPC diff --git a/proto b/proto index 97c11c6..0428726 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 97c11c6951bb47a5170b2f0580f892c738fcb63f +Subproject commit 042872669279d137bc48a0da6d0f22c55a97af8d From 86f5c4f8e99784013da261a845a49dea5f7329b2 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 16:33:37 +0300 Subject: [PATCH 38/86] fix(tests,langchain,demo): use params keyword, assert schema_mode, pin demo image version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - langchain graph.py: pass params={} as keyword argument to cypher() - test_sdk.py: assert schema_mode is int in (0, 3) in flexible-mode test; document RETURN n → Value::Int(node_id) in FTS tests - demo/docker-compose.yml: pin coordinode:0.3.11 (was :latest) --- demo/docker-compose.yml | 42 +++++++++++++++++++ .../langchain_coordinode/graph.py | 2 +- tests/integration/test_sdk.py | 5 +++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 demo/docker-compose.yml diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml new file mode 100644 index 0000000..81f983b --- /dev/null +++ b/demo/docker-compose.yml @@ -0,0 +1,42 @@ +services: + coordinode: + image: ghcr.io/structured-world/coordinode:0.3.11 + container_name: demo-coordinode + ports: + - "37080:7080" # gRPC (native API) + - "37084:7084" # Prometheus /metrics, /health + volumes: + - coordinode-data:/data + environment: + - COORDINODE_LOG_FORMAT=text + restart: unless-stopped + healthcheck: + test: ["CMD", "/coordinode", "version"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s + + jupyter: + build: + context: . + dockerfile: Dockerfile.jupyter + container_name: demo-jupyter + ports: + - "38888:8888" # Jupyter Lab + volumes: + - ./notebooks:/home/jovyan/work + - ../:/sdk # mount SDK source so notebooks can pip install -e + environment: + - COORDINODE_ADDR=coordinode:7080 # internal network address + - JUPYTER_ENABLE_LAB=yes + - JUPYTER_TOKEN=demo + - SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 # hatch-vcs: skip git, use fixed version + depends_on: + coordinode: + condition: service_healthy + restart: unless-stopped + +volumes: + coordinode-data: + driver: local diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index d98c592..f603faa 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -154,7 +154,7 @@ def refresh_schema(self) -> None: # published Docker image (tracked in G010 / GAPS.md). rows = self._client.cypher( "MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels", - {}, + params={}, ) if rows: triples: set[tuple[str, str, str]] = set() diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index f7a7445..fc9d47b 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -489,6 +489,8 @@ def test_create_label_schema_mode_flexible(client): info = client.create_label(name, schema_mode="flexible") assert isinstance(info, LabelInfo) assert info.name == name + assert isinstance(info.schema_mode, int) + assert info.schema_mode in (0, 3) # 0 on older servers, 3 = FLEXIBLE def test_create_label_invalid_schema_mode_raises(client): @@ -593,6 +595,8 @@ def test_text_search_returns_results(client): """text_search() finds nodes whose text property matches the query.""" label = f"FtsTest_{uid()}" tag = uid() + # CoordiNode executor serialises a node variable as Value::Int(node_id) — runner.rs NodeScan + # path. No id() function needed; rows[0]["node_id"] is the integer internal node id. rows = client.cypher( f"CREATE (n:{label} {{tag: $tag, body: 'machine learning and neural networks'}}) RETURN n AS node_id", params={"tag": tag}, @@ -648,6 +652,7 @@ def test_hybrid_text_vector_search_returns_results(client): label = f"FtsHybridTest_{uid()}" tag = uid() vec = [float(i) / 16 for i in range(16)] + # Same node-as-int pattern: RETURN n → Value::Int(node_id) in CoordiNode executor. rows = client.cypher( f"CREATE (n:{label} {{tag: $tag, body: 'graph neural network embedding', embedding: $vec}}) RETURN n AS node_id", params={"tag": tag, "vec": vec}, From bc9fc69f47c0b2457a75980f9f211a136ad2a9bc Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:09:54 +0300 Subject: [PATCH 39/86] fix(demo): use HTTP /health readiness probe in healthcheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `/coordinode version` (binary check only) with `wget -qO- http://localhost:7084/health` which actually verifies the serve port is accepting requests — same probe as root docker-compose.yml. --- demo/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 81f983b..4229b9c 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -11,7 +11,7 @@ services: - COORDINODE_LOG_FORMAT=text restart: unless-stopped healthcheck: - test: ["CMD", "/coordinode", "version"] + test: ["CMD-SHELL", "wget -qO- http://localhost:7084/health || exit 1"] interval: 5s timeout: 3s retries: 10 From 1b685ba56a1e09073db499914a3b1aeefb0a1b0a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:12:02 +0300 Subject: [PATCH 40/86] docs(demo): explain IN_COLAB gate and demo token intent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In 00_seed_data.ipynb: add comment explaining why no extra COORDINODE_ENABLE_RUSTUP env var is needed — IN_COLAB already scopes the rustup install to Colab sessions only - In demo/docker-compose.yml: annotate JUPYTER_TOKEN=demo as intentional (localhost-only demo stack, documented in README) --- demo/docker-compose.yml | 2 +- demo/notebooks/00_seed_data.ipynb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 4229b9c..72fb9f8 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -30,7 +30,7 @@ services: environment: - COORDINODE_ADDR=coordinode:7080 # internal network address - JUPYTER_ENABLE_LAB=yes - - JUPYTER_TOKEN=demo + - JUPYTER_TOKEN=demo # intentional: localhost-only demo stack, documented in README ("token: demo") - SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 # hatch-vcs: skip git, use fixed version depends_on: coordinode: diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 1b97133..10e5a01 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", From b23c833ae46fa040fad59d3bd116a251f8eb2001 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:47:24 +0300 Subject: [PATCH 41/86] build: bump coordinode-rs to v0.3.12, proto to DecommissionNode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - coordinode-rs: v0.3.11 → v0.3.12 (node decommission protocol, unified Raft write path, cluster integration tests for R091c) - proto: 0428726 → 35b39be (DecommissionNode RPC in AdminService) - docker-compose.yml + demo/docker-compose.yml: image → 0.3.12 --- coordinode-rs | 2 +- demo/docker-compose.yml | 2 +- docker-compose.yml | 2 +- proto | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index 0ff4b47..492d7ea 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 0ff4b47693ab9b99599cb9bdb8b7532f03aa7f6e +Subproject commit 492d7eaacb7b961f21605c1a105ed14818abdc25 diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 72fb9f8..5cfaac9 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,6 +1,6 @@ services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.11 + image: ghcr.io/structured-world/coordinode:0.3.12 container_name: demo-coordinode ports: - "37080:7080" # gRPC (native API) diff --git a/docker-compose.yml b/docker-compose.yml index f3f8f63..8d26305 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ version: "3.9" services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.11 + image: ghcr.io/structured-world/coordinode:0.3.12 container_name: coordinode ports: - "7080:7080" # gRPC diff --git a/proto b/proto index 0428726..35b39be 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 042872669279d137bc48a0da6d0f22c55a97af8d +Subproject commit 35b39be778f41eec71b5801c62e243a8a3b39364 From 678ee5f5592dc81a3404e63a17729ab9933c8332 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:47:39 +0300 Subject: [PATCH 42/86] docs(demo): pin coordinode-embedded install to b23c833 (coordinode-rs v0.3.12) Colab notebooks now install coordinode-embedded at the exact commit that pins coordinode-rs to v0.3.12, ensuring reproducible Rust builds. --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 4 ++-- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 10e5a01..6df1bc8 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -64,7 +64,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 6cbc416..7a02464 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -109,7 +109,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index c025969..f48d51c 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -106,7 +106,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 36621df..07413b0 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -56,7 +56,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", From f8901d72cfa3aaeaf3c6d637542e11353cb37cb3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:54:28 +0300 Subject: [PATCH 43/86] fix: remove MERGE workarounds, tighten assertions, bind Jupyter localhost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - graph.py _create_edge(): remove if/else split around SET r += $props — SET r += {} is supported since coordinode-rs v0.3.12 - base.py upsert_relations(): same simplification - test_sdk.py: assert schema_mode == 3 (FLEXIBLE now enforced by server, remove fallback for older servers that returned 0) - client.py _build_property_definitions(): add isinstance(raw_type, str) guard and .strip() before .lower() — consistent with schema_mode normalization - demo/docker-compose.yml: bind Jupyter to 127.0.0.1:38888 (explicit localhost-only, matches documented intent) - demo/notebooks 01-03: add IN_COLAB gate explanation comment (same as nb00) --- coordinode/coordinode/client.py | 8 ++++-- demo/docker-compose.yml | 2 +- .../01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 2 +- .../langchain_coordinode/graph.py | 26 +++++-------------- .../graph_stores/coordinode/base.py | 23 ++++++---------- tests/integration/test_sdk.py | 3 +-- 8 files changed, 26 insertions(+), 42 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 9d62b91..b97dd16 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -427,10 +427,14 @@ def _build_property_definitions( name = p.get("name") if not isinstance(name, str) or not name: raise ValueError(f"Property at index {idx} must have a non-empty 'name' key; got {p!r}") - type_str = str(p.get("type", "string")).lower() + raw_type = p.get("type", "string") + if "type" in p and not isinstance(raw_type, str): + raise ValueError(f"Property {name!r} must use a string value for 'type'; got {raw_type!r}") + type_str = str(raw_type).strip().lower() if type_str not in type_map: raise ValueError( - f"Unknown property type {type_str!r} for property {name!r}. Expected one of: {sorted(type_map)}" + f"Unknown property type {type_str!r} for property {name!r}. " + f"Expected 'type' to be one of: {sorted(type_map)}" ) required = p.get("required", False) unique = p.get("unique", False) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 5cfaac9..99e1671 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -23,7 +23,7 @@ services: dockerfile: Dockerfile.jupyter container_name: demo-jupyter ports: - - "38888:8888" # Jupyter Lab + - "127.0.0.1:38888:8888" # Jupyter Lab (localhost-only) volumes: - ./notebooks:/home/jovyan/work - ../:/sdk # mount SDK source so notebooks can pip install -e diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 7a02464..9b06144 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index f48d51c..5795820 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 07413b0..fdc915d 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index f603faa..b117cfe 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -210,29 +210,17 @@ def _upsert_node(self, node: Any) -> None: ) def _create_edge(self, rel: Any) -> None: - """Upsert a relationship via MERGE (idempotent). - - SET r += $props is skipped when props is empty because - SET r += {} is not supported by all server versions. - """ + """Upsert a relationship via MERGE (idempotent).""" src_label = _cypher_ident(rel.source.type or "Entity") dst_label = _cypher_ident(rel.target.type or "Entity") rel_type = _cypher_ident(rel.type) props = dict(rel.properties or {}) - if props: - self._client.cypher( - f"MATCH (src:{src_label} {{name: $src}}) " - f"MATCH (dst:{dst_label} {{name: $dst}}) " - f"MERGE (src)-[r:{rel_type}]->(dst) SET r += $props", - params={"src": rel.source.id, "dst": rel.target.id, "props": props}, - ) - else: - self._client.cypher( - f"MATCH (src:{src_label} {{name: $src}}) " - f"MATCH (dst:{dst_label} {{name: $dst}}) " - f"MERGE (src)-[r:{rel_type}]->(dst)", - params={"src": rel.source.id, "dst": rel.target.id}, - ) + self._client.cypher( + f"MATCH (src:{src_label} {{name: $src}}) " + f"MATCH (dst:{dst_label} {{name: $dst}}) " + f"MERGE (src)-[r:{rel_type}]->(dst) SET r += $props", + params={"src": rel.source.id, "dst": rel.target.id, "props": props}, + ) def _link_document_to_entities(self, doc: Any) -> None: """Upsert a ``__Document__`` node and MERGE ``MENTIONS`` edges to all entities.""" diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 3cc65fd..960ca4d 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -240,21 +240,14 @@ def upsert_relations(self, relations: list[Relation]) -> None: for rel in relations: props = rel.properties or {} label = _cypher_ident(rel.label) - if props: - cypher = ( - f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) " - f"MERGE (src)-[r:{label}]->(dst) SET r += $props" - ) - self._client.cypher( - cypher, - params={"src_id": rel.source_id, "dst_id": rel.target_id, "props": props}, - ) - else: - cypher = f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) MERGE (src)-[r:{label}]->(dst)" - self._client.cypher( - cypher, - params={"src_id": rel.source_id, "dst_id": rel.target_id}, - ) + cypher = ( + f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) " + f"MERGE (src)-[r:{label}]->(dst) SET r += $props" + ) + self._client.cypher( + cypher, + params={"src_id": rel.source_id, "dst_id": rel.target_id, "props": props}, + ) def delete( self, diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index fc9d47b..08379c4 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -489,8 +489,7 @@ def test_create_label_schema_mode_flexible(client): info = client.create_label(name, schema_mode="flexible") assert isinstance(info, LabelInfo) assert info.name == name - assert isinstance(info.schema_mode, int) - assert info.schema_mode in (0, 3) # 0 on older servers, 3 = FLEXIBLE + assert info.schema_mode == 3 # FLEXIBLE def test_create_label_invalid_schema_mode_raises(client): From 386b28023af356edbc804ee208a507f54d8f21af Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 18:55:21 +0300 Subject: [PATCH 44/86] docs(demo): note pinned coordinode-rs v0.3.12 in README --- demo/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/README.md b/demo/README.md index 7b484b6..a672b66 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,6 +13,7 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. +> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.12. ## Run locally (Docker Compose) From b318ece4eaa3cfa532187dcae96d094da584d2c6 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 19:13:23 +0300 Subject: [PATCH 45/86] style: ruff format nb00 after IN_COLAB comment addition --- demo/notebooks/00_seed_data.ipynb | 143 +++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 3 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 6df1bc8..2978f49 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,76 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when no gRPC server is available (Colab path).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded only when no gRPC server is available (Colab path).\n", + "if IN_COLAB:\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", + " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", + " # platform-specific rustup-init binaries), and pinning a hash here would break\n", + " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", + " # execution (not piped to shell) is the rustup team's recommended trust model.\n", + " # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n", + " # the `IN_COLAB` check above already ensures this block never runs outside\n", + " # Colab sessions, so there is no risk of unintentional execution in local\n", + " # or server environments.\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"Ready\")" + ] }, { "cell_type": "markdown", @@ -64,7 +133,46 @@ "id": "a1b2c3d4-0000-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" + "source": [ + "import os, socket\n", + "\n", + "\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" + ] }, { "cell_type": "markdown", @@ -297,7 +405,36 @@ "id": "a1b2c3d4-0000-0000-0000-000000000014", "metadata": {}, "outputs": [], - "source": "print(\"=== Who works at Synthex? ===\")\nrows = client.cypher(\n \"MATCH (p:Person {demo_tag: $tag})-[:WORKS_AT]->(c:Company {name: $co, demo_tag: $tag}) \"\n \"RETURN p.name AS name, p.role AS role\",\n params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n)\nfor r in rows:\n print(f\" {r['name']} — {r['role']}\")\n\nprint(\"\\n=== What does Synthex use? ===\")\nrows = client.cypher(\n \"MATCH (c:Company {name: $co, demo_tag: $tag})-[:USES]->(t:Technology {demo_tag: $tag}) RETURN t.name AS name\",\n params={\"co\": \"Synthex\", \"tag\": DEMO_TAG}\n)\nfor r in rows:\n print(f\" {r['name']}\")\n\nprint(\"\\n=== GraphRAG dependency chain ===\")\nrows = client.cypher(\n \"MATCH (t:Technology {name: $tech, demo_tag: $tag})-[:BUILDS_ON*1..3]->(dep:Technology {demo_tag: $tag}) RETURN dep.name AS dependency\",\n params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n)\nfor r in rows:\n print(f\" → {r['dependency']}\")\n\nprint(\"\\n✓ Demo data seeded.\")\nprint(\"To query it from notebooks 01–03, connect them to the same CoordiNode server (COORDINODE_ADDR).\")\nclient.close()" + "source": [ + "print(\"=== Who works at Synthex? ===\")\n", + "rows = client.cypher(\n", + " \"MATCH (p:Person {demo_tag: $tag})-[:WORKS_AT]->(c:Company {name: $co, demo_tag: $tag}) \"\n", + " \"RETURN p.name AS name, p.role AS role\",\n", + " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n", + ")\n", + "for r in rows:\n", + " print(f\" {r['name']} — {r['role']}\")\n", + "\n", + "print(\"\\n=== What does Synthex use? ===\")\n", + "rows = client.cypher(\n", + " \"MATCH (c:Company {name: $co, demo_tag: $tag})-[:USES]->(t:Technology {demo_tag: $tag}) RETURN t.name AS name\",\n", + " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n", + ")\n", + "for r in rows:\n", + " print(f\" {r['name']}\")\n", + "\n", + "print(\"\\n=== GraphRAG dependency chain ===\")\n", + "rows = client.cypher(\n", + " \"MATCH (t:Technology {name: $tech, demo_tag: $tag})-[:BUILDS_ON*1..3]->(dep:Technology {demo_tag: $tag}) RETURN dep.name AS dependency\",\n", + " params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n", + ")\n", + "for r in rows:\n", + " print(f\" → {r['dependency']}\")\n", + "\n", + "print(\"\\n✓ Demo data seeded.\")\n", + "print(\"To query it from notebooks 01–03, connect them to the same CoordiNode server (COORDINODE_ADDR).\")\n", + "client.close()" + ] } ], "metadata": { From 5a304bb003a5e194fc4b80e2f5e7b92d079b513c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:22:30 +0300 Subject: [PATCH 46/86] =?UTF-8?q?docs(client):=20update=20text=5Fsearch()?= =?UTF-8?q?=20docstring=20=E2=80=94=20FTS=20is=20automatic,=20no=20pre-reg?= =?UTF-8?q?istered=20index=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coordinode/coordinode/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index b97dd16..e94908a 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -588,8 +588,10 @@ async def text_search( """Run a full-text BM25 search over all indexed text properties for *label*. Args: - label: Node label to search (e.g. ``"Article"``). Must have at least - one text index registered; returns ``[]`` otherwise. + label: Node label to search (e.g. ``"Article"``). On current + CoordiNode servers, text properties in schema-free graphs are + indexed automatically; labels with no searchable text content + simply return ``[]``. query: Full-text query string. Supports boolean operators (``AND``, ``OR``, ``NOT``), phrase search (``"exact phrase"``), prefix wildcards (``term*``), and per-term boosting (``term^N``). From 470adacaaa37f2a78bcd628812ba2ced2eb4c243 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:23:33 +0300 Subject: [PATCH 47/86] docs(demo): clarify Dockerfile.jupyter path in compose comment --- demo/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 99e1671..88b465a 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -20,7 +20,7 @@ services: jupyter: build: context: . - dockerfile: Dockerfile.jupyter + dockerfile: Dockerfile.jupyter # demo/Dockerfile.jupyter — builds Jupyter Lab with SDK installed container_name: demo-jupyter ports: - "127.0.0.1:38888:8888" # Jupyter Lab (localhost-only) From 3811d093c708e3f49d194a68483d2169c952d3a8 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:24:15 +0300 Subject: [PATCH 48/86] fix(demo): use unique DEMO_TAG default to avoid accidental data deletion between runs --- demo/notebooks/00_seed_data.ipynb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 2978f49..2bac5e7 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -189,7 +189,10 @@ "metadata": {}, "outputs": [], "source": [ - "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\", \"seed_data\")\n", + "import uuid\n", + "\n", + "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\n", + "print(\"Using DEMO_TAG:\", DEMO_TAG)\n", "result = client.cypher(\n", " \"MATCH (n {demo: true, demo_tag: $tag}) DETACH DELETE n\",\n", " params={\"tag\": DEMO_TAG},\n", From 6c8f2d71529ed9061d44149dbcf3d66c31a74ab4 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:24:50 +0300 Subject: [PATCH 49/86] fix(demo): scope c:Entity to session in nb03 Cypher example query --- demo/notebooks/03_langgraph_agent.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index fdc915d..3d83512 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -115,7 +115,7 @@ "print(\n", " query_facts.invoke(\n", " {\n", - " \"cypher\": 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\"}) RETURN p.name AS employee'\n", + " \"cypher\": 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\", session: $sess}) RETURN p.name AS employee'\n", " }\n", " )\n", ")" From 767ab05149be5e067a61c2a8360264c3195c84c6 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:27:48 +0300 Subject: [PATCH 50/86] fix(demo): skip embedded Rust build in nb03 when COORDINODE_ADDR is already set --- demo/notebooks/03_langgraph_agent.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 3d83512..1790277 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", From 5ca8737bdcab777d0eb8661c7c6a47cf5e8278a3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 14 Apr 2026 21:28:06 +0300 Subject: [PATCH 51/86] docs(test): explain why schema_mode == 3 is intentional (v0.3.12+ enforces FLEXIBLE) --- tests/integration/test_sdk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 08379c4..169f2ae 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -489,6 +489,9 @@ def test_create_label_schema_mode_flexible(client): info = client.create_label(name, schema_mode="flexible") assert isinstance(info, LabelInfo) assert info.name == name + # FLEXIBLE (SchemaMode=3) is enforced server-side since coordinode-rs v0.3.12. + # Older servers that omit the field return 0 (default), but this test suite + # targets v0.3.12+ and asserts the concrete value to catch regressions. assert info.schema_mode == 3 # FLEXIBLE From c0f040076fd2a46b8097a1fffd4183fec7ad303e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 00:01:36 +0300 Subject: [PATCH 52/86] fix(demo,tests): bump coordinode to v0.3.14, harden port binding and cleanup - bump coordinode server image to v0.3.14 in docker-compose.yml - update coordinode-rs submodule to latest main (v0.3.13 B-tree index and DETACH DELETE fixes included) - bind demo ports to 127.0.0.1 to prevent LAN exposure (gRPC 37080, Prometheus 37084) - skip embedded Rust build in 00_seed_data.ipynb when COORDINODE_ADDR is set (live server available) - replace DETACH DELETE with two-step edge+node DELETE for compatibility across all CoordiNode versions - add Dockerfile.jupyter context comment to clarify dockerfile location - extend test_text_search_empty_for_unindexed_label to cover label with no text properties in addition to unknown-label case --- coordinode-rs | 2 +- demo/docker-compose.yml | 10 ++-- demo/notebooks/00_seed_data.ipynb | 85 ++----------------------------- tests/integration/test_sdk.py | 22 +++++++- 4 files changed, 31 insertions(+), 88 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index 492d7ea..8dc198f 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 492d7eaacb7b961f21605c1a105ed14818abdc25 +Subproject commit 8dc198f7e00d6f363edd9a6dce4695bbea23c9a1 diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 88b465a..2b79cb9 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,10 +1,10 @@ services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.12 + image: ghcr.io/structured-world/coordinode:0.3.14 container_name: demo-coordinode ports: - - "37080:7080" # gRPC (native API) - - "37084:7084" # Prometheus /metrics, /health + - "127.0.0.1:37080:7080" # gRPC (native API) — localhost-only + - "127.0.0.1:37084:7084" # Prometheus /metrics, /health — localhost-only volumes: - coordinode-data:/data environment: @@ -20,7 +20,9 @@ services: jupyter: build: context: . - dockerfile: Dockerfile.jupyter # demo/Dockerfile.jupyter — builds Jupyter Lab with SDK installed + # Dockerfile.jupyter lives in demo/ (same context directory as this file). + # It builds a Jupyter Lab image with the SDK installed from the SDK source mount. + dockerfile: Dockerfile.jupyter container_name: demo-jupyter ports: - "127.0.0.1:38888:8888" # Jupyter Lab (localhost-only) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 2bac5e7..bf5c5f8 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,76 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": [ - "import os, sys, subprocess\n", - "\n", - "IN_COLAB = \"google.colab\" in sys.modules\n", - "\n", - "# Install coordinode-embedded only when no gRPC server is available (Colab path).\n", - "if IN_COLAB:\n", - " # Install Rust toolchain via rustup (https://rustup.rs).\n", - " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", - " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", - " # Download the installer to a temp file and execute it explicitly — this avoids\n", - " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", - " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", - " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", - " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", - " # platform-specific rustup-init binaries), and pinning a hash here would break\n", - " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", - " # execution (not piped to shell) is the rustup team's recommended trust model.\n", - " # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n", - " # the `IN_COLAB` check above already ensures this block never runs outside\n", - " # Colab sessions, so there is no risk of unintentional execution in local\n", - " # or server environments.\n", - " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", - "\n", - " _ctx = _ssl.create_default_context()\n", - " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", - " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", - " _f.write(_r.read())\n", - " _rustup_path = _f.name\n", - " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", - " finally:\n", - " os.unlink(_rustup_path)\n", - " # Add cargo to PATH so maturin/pip can find it.\n", - " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", - " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", - " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", - " subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n", - " ],\n", - " check=True,\n", - " timeout=600,\n", - " )\n", - "\n", - "subprocess.run(\n", - " [\n", - " sys.executable,\n", - " \"-m\",\n", - " \"pip\",\n", - " \"install\",\n", - " \"-q\",\n", - " \"coordinode\",\n", - " \"nest_asyncio\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - ")\n", - "\n", - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "print(\"Ready\")" - ] + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -188,17 +119,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": [ - "import uuid\n", - "\n", - "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\n", - "print(\"Using DEMO_TAG:\", DEMO_TAG)\n", - "result = client.cypher(\n", - " \"MATCH (n {demo: true, demo_tag: $tag}) DETACH DELETE n\",\n", - " params={\"tag\": DEMO_TAG},\n", - ")\n", - "print(\"Previous demo data removed\")" - ] + "source": "import uuid\n\nDEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\nprint(\"Using DEMO_TAG:\", DEMO_TAG)\n# Delete relationships first, then nodes — two-step ensures correct cleanup across\n# all CoordiNode versions (embedded and server).\nclient.cypher(\n \"MATCH (n {demo: true, demo_tag: $tag})-[r]-() DELETE r\",\n params={\"tag\": DEMO_TAG},\n)\nclient.cypher(\n \"MATCH (n {demo: true, demo_tag: $tag}) DELETE n\",\n params={\"tag\": DEMO_TAG},\n)\nprint(\"Previous demo data removed\")" }, { "cell_type": "markdown", @@ -452,4 +373,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 169f2ae..1a905bd 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -624,10 +624,30 @@ def test_text_search_returns_results(client): @_fts def test_text_search_empty_for_unindexed_label(client): - """text_search() returns [] for a label with no text index (no error).""" + """text_search() returns [] when there are no text-indexed nodes to match. + + Covers two distinct cases: + 1. Label has never been inserted — nothing to search. + 2. Label exists but nodes carry only numeric/boolean properties; the FTS + index contains no text, so no results can match any query term. + """ + # Case 1: label that has never been inserted into the graph results = client.text_search("NoSuchLabelForFts_" + uid(), "anything") assert results == [] + # Case 2: label exists but nodes have no text properties to index + label = f"FtsNumericOnly_{uid()}" + tag = uid() + client.cypher( + f"CREATE (n:{label} {{tag: $tag, count: 42, active: true}})", + params={"tag": tag}, + ) + try: + results = client.text_search(label, "anything") + assert results == [] + finally: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + @_fts def test_text_search_fuzzy(client): From 8eefdc4c6493806446e93cf2c2f9e033de1c6d8f Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 01:43:18 +0300 Subject: [PATCH 53/86] build: align coordinode image to v0.3.14 across root compose and demo README Root docker-compose.yml and demo/README.md still referenced v0.3.12 while demo/docker-compose.yml was already on v0.3.14. Aligns all references to v0.3.14 to avoid mismatched server/client/proto combinations. --- demo/README.md | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/README.md b/demo/README.md index a672b66..62a10df 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,7 +13,7 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. -> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.12. +> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.13. ## Run locally (Docker Compose) diff --git a/docker-compose.yml b/docker-compose.yml index 8d26305..72b85f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ version: "3.9" services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.12 + image: ghcr.io/structured-world/coordinode:0.3.14 container_name: coordinode ports: - "7080:7080" # gRPC From 1aaca85579d21256f4a902a0c5a6159d90b97bee Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:16:42 +0300 Subject: [PATCH 54/86] build: add sync comment to demo compose coordinode image version --- demo/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 2b79cb9..3d2c98c 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,5 +1,6 @@ services: coordinode: + # Keep version in sync with root docker-compose.yml image: ghcr.io/structured-world/coordinode:0.3.14 container_name: demo-coordinode ports: From d5d2f7fb395adc5d848ede8c43565379531af990 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:17:48 +0300 Subject: [PATCH 55/86] fix(demo,tests): address review threads #145 #146 #147 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nb03: add client.health() verification before 'Connected' print in both gRPC branches (COORDINODE_ADDR env and _port_open fallback) - nb03: scope find_related intermediate hops to session — use path variable p and WHERE ALL(x IN nodes(p) WHERE x.session = $sess); switch type(last(r)) to type(last(relationships(p))) - tests: add test_create_label_schema_mode_validated asserting schema_mode == 2 (VALIDATED) to complement flexible/invalid tests --- demo/notebooks/03_langgraph_agent.ipynb | 138 +++++++++++++++++++++++- tests/integration/test_sdk.py | 9 ++ 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 1790277..b999e19 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -56,7 +56,48 @@ "id": "d4e5f6a7-0003-0000-0000-000000000005", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n client = LocalClient(\":memory:\")\n print(\"Using embedded LocalClient (in-process)\")" + "source": [ + "import os, socket\n", + "\n", + "\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + "\n", + "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "elif _port_open(GRPC_PORT):\n", + " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " # No server available — use the embedded in-process engine.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" + ] }, { "cell_type": "markdown", @@ -75,7 +116,98 @@ "id": "d4e5f6a7-0003-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH (n:Entity {{name: $name, session: $sess}})-[r*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"RETURN m.name AS related, type(last(r)) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" + "source": [ + "import os, re, uuid\n", + "from langchain_core.tools import tool\n", + "\n", + "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", + "\n", + "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "# Regex guards for query_facts (demo safety guard).\n", + "_WRITE_CLAUSE_RE = re.compile(\n", + " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", + "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", + "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", + "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", + "# In production code, use server-side row-level security instead of client regex.\n", + "_SESSION_WHERE_SCOPE_RE = re.compile(\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "_SESSION_NODE_SCOPE_RE = re.compile(\n", + " r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "\n", + "\n", + "@tool\n", + "def save_fact(subject: str, relation: str, obj: str) -> str:\n", + " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", + " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " rel_type = relation.upper().replace(\" \", \"_\")\n", + " # Validate rel_type before interpolating into Cypher to prevent injection.\n", + " if not _REL_TYPE_RE.fullmatch(rel_type):\n", + " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", + " client.cypher(\n", + " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", + " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", + " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", + " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", + " )\n", + " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", + "\n", + "\n", + "@tool\n", + "def query_facts(cypher: str) -> str:\n", + " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", + " Must scope reads via either WHERE .session = $sess\n", + " or a node pattern {session: $sess}.\"\"\"\n", + " q = cypher.strip()\n", + " if _WRITE_CLAUSE_RE.search(q):\n", + " return \"Only read-only Cypher is allowed in query_facts.\"\n", + " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", + " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", + " if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n", + " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", + " rows = client.cypher(q, params={\"sess\": SESSION})\n", + " return str(rows[:20]) if rows else \"No results\"\n", + "\n", + "\n", + "@tool\n", + "def find_related(entity_name: str, depth: int = 1) -> str:\n", + " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " safe_depth = max(1, min(int(depth), 3))\n", + " rows = client.cypher(\n", + " f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", + " \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n", + " \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n", + " params={\"name\": entity_name, \"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return f\"No related entities found for {entity_name}\"\n", + " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", + "\n", + "@tool\n", + "def list_all_facts() -> str:\n", + " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", + " rows = client.cypher(\n", + " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", + " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", + " params={\"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return \"No facts stored yet\"\n", + " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "\n", + "\n", + "tools = [save_fact, query_facts, find_related, list_all_facts]\n", + "print(f\"Session: {SESSION}\")\n", + "print(\"Tools:\", [t.name for t in tools])" + ] }, { "cell_type": "markdown", @@ -256,4 +388,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 1a905bd..b8175e2 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -495,6 +495,15 @@ def test_create_label_schema_mode_flexible(client): assert info.schema_mode == 3 # FLEXIBLE +def test_create_label_schema_mode_validated(client): + """create_label() with schema_mode='validated' is accepted and returns SchemaMode=2.""" + name = f"ValidatedLabel{uid()}" + info = client.create_label(name, schema_mode="validated") + assert isinstance(info, LabelInfo) + assert info.name == name + assert info.schema_mode == 2 # VALIDATED + + def test_create_label_invalid_schema_mode_raises(client): """create_label() with unknown schema_mode raises ValueError locally.""" with pytest.raises(ValueError, match="schema_mode"): From e06be1f1489b6199333dc77573482cadbae1a262 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:19:15 +0300 Subject: [PATCH 56/86] fix(demo): check health() return value before printing Connected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit client.health() returns bool, not raises — bare call ignored the result. Now raise RuntimeError with a clear message if health check fails. --- demo/notebooks/03_langgraph_agent.ipynb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index b999e19..34c1f43 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -75,14 +75,18 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " client.close()\n", + " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "elif _port_open(GRPC_PORT):\n", " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " client.close()\n", + " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " # No server available — use the embedded in-process engine.\n", From 27434ee2661e9105756c5b83e0ebc605d80d75f3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:56:26 +0300 Subject: [PATCH 57/86] feat(sdk,tests): add create_text_index/drop_text_index, fix FTS test assumptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FTS indexing is NOT automatic — a CREATE TEXT INDEX DDL is required before text_search() can return results. The previous code and tests incorrectly assumed automatic indexing. SDK changes: - Add TextIndexInfo result class (name, label, properties, documents_indexed) - Add AsyncCoordinodeClient.create_text_index() and drop_text_index() - Add CoordinodeClient sync wrappers for both methods - Export TextIndexInfo from coordinode package - Fix text_search() docstring: document DDL requirement, remove false claim about automatic indexing Test changes: - Fix test_text_search_returns_results: create/drop TEXT INDEX around the search call; drop empty-results xfail (now a hard assertion) - Fix test_text_search_fuzzy: create/drop TEXT INDEX; assert results are non-empty (was: just verify no exception) - Fix test_hybrid_text_vector_search_returns_results: add TEXT INDEX - Update header comment: remove 'automatic indexing' statement --- coordinode/coordinode/__init__.py | 2 + coordinode/coordinode/client.py | 93 +++++++++++++++++++++++++++++++ tests/integration/test_sdk.py | 38 ++++++++----- 3 files changed, 118 insertions(+), 15 deletions(-) diff --git a/coordinode/coordinode/__init__.py b/coordinode/coordinode/__init__.py index ddb3482..423f831 100644 --- a/coordinode/coordinode/__init__.py +++ b/coordinode/coordinode/__init__.py @@ -27,6 +27,7 @@ LabelInfo, NodeResult, PropertyDefinitionInfo, + TextIndexInfo, TextResult, TraverseResult, VectorResult, @@ -47,5 +48,6 @@ "LabelInfo", "EdgeTypeInfo", "PropertyDefinitionInfo", + "TextIndexInfo", "TraverseResult", ] diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index e94908a..42b20a2 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -161,6 +161,23 @@ def __repr__(self) -> str: return f"TraverseResult(nodes={len(self.nodes)}, edges={len(self.edges)})" +class TextIndexInfo: + """Information about a full-text index returned by :meth:`create_text_index`.""" + + def __init__(self, row: dict[str, Any]) -> None: + self.name: str = str(row.get("index", "")) + self.label: str = str(row.get("label", "")) + self.properties: str = str(row.get("properties", "")) + self.default_language: str = str(row.get("default_language", "")) + self.documents_indexed: int = int(row.get("documents_indexed", 0)) + + def __repr__(self) -> str: + return ( + f"TextIndexInfo(name={self.name!r}, label={self.label!r}," + f" properties={self.properties!r}, documents_indexed={self.documents_indexed})" + ) + + # ── Async client ───────────────────────────────────────────────────────────── @@ -524,6 +541,57 @@ async def create_edge_type( et = await self._schema_stub.CreateEdgeType(req, timeout=self._timeout) return EdgeTypeInfo(et) + async def create_text_index( + self, + name: str, + label: str, + properties: str | list[str], + *, + language: str = "", + ) -> TextIndexInfo: + """Create a full-text (BM25) index on one or more node properties. + + Args: + name: Unique index name (e.g. ``"article_body"``). + label: Node label to index (e.g. ``"Article"``). + properties: Property name or list of property names to index + (e.g. ``"body"`` or ``["title", "body"]``). + language: Default stemming/tokenization language (e.g. ``"english"``, + ``"russian"``). Empty string uses the server default + (``"english"``). + + Returns: + :class:`TextIndexInfo` with index metadata and document count. + + Example:: + + info = await client.create_text_index("article_body", "Article", "body") + # then: results = await client.text_search("Article", "machine learning") + """ + if isinstance(properties, str): + prop_list = [properties] + else: + prop_list = list(properties) + props_expr = ", ".join(prop_list) + lang_clause = f" DEFAULT LANGUAGE {language}" if language else "" + cypher = f"CREATE TEXT INDEX {name} ON :{label}({props_expr}){lang_clause}" + rows = await self.cypher(cypher) + if rows: + return TextIndexInfo(rows[0]) + return TextIndexInfo({"index": name, "label": label, "properties": ", ".join(prop_list)}) + + async def drop_text_index(self, name: str) -> None: + """Drop a full-text index by name. + + Args: + name: Index name previously passed to :meth:`create_text_index`. + + Example:: + + await client.drop_text_index("article_body") + """ + await self.cypher(f"DROP TEXT INDEX {name}") + async def traverse( self, start_node_id: int, @@ -604,6 +672,16 @@ async def text_search( Returns: List of :class:`TextResult` ordered by BM25 score descending. + Returns ``[]`` if no text index exists for *label*. + + Note: + Text indexing is **not** automatic. Before calling this method, + create a full-text index with the Cypher DDL statement:: + + CREATE TEXT INDEX my_index ON :Label(property) + + or via :meth:`create_text_index`. Nodes written before the index + was created are indexed immediately at DDL execution time. """ from coordinode._proto.coordinode.v1.query.text_pb2 import TextSearchRequest # type: ignore[import] @@ -806,6 +884,21 @@ def create_edge_type( """Create an edge type in the schema registry.""" return self._run(self._async.create_edge_type(name, properties)) + def create_text_index( + self, + name: str, + label: str, + properties: str | list[str], + *, + language: str = "", + ) -> TextIndexInfo: + """Create a full-text (BM25) index on one or more node properties.""" + return self._run(self._async.create_text_index(name, label, properties, language=language)) + + def drop_text_index(self, name: str) -> None: + """Drop a full-text index by name.""" + return self._run(self._async.drop_text_index(name)) + def traverse( self, start_node_id: int, diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index b8175e2..1c75da8 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -20,6 +20,7 @@ EdgeTypeInfo, HybridResult, LabelInfo, + TextIndexInfo, TextResult, TraverseResult, ) @@ -566,16 +567,13 @@ def test_vector_search_returns_results(client): # FTS tests require a CoordiNode server with TextService implemented (>=0.3.8). -# They are marked xfail so the suite stays green against older servers; once -# upgraded, the tests turn into expected passes automatically. +# They are wrapped with @_fts so the suite stays green against older servers +# (UNIMPLEMENTED gRPC status → xfail); against >=0.3.8 servers they are real passes. # -# Note: create_label() is intentionally NOT called before text_search(). -# FTS indexing in CoordiNode is automatic for all nodes whose label was written -# via CREATE/MERGE — no explicit label registration is required. On schema-strict -# servers a caller may choose to pre-register a label, but the SDK's text_search() -# and hybrid_text_vector_search() work on schema-free graphs too. These tests -# exercise the common schema-free path; calling create_label() here would test a -# different (schema-strict) code path and is covered by test_create_label_*. +# FTS indexing is NOT automatic. Each test that expects non-empty results must +# first create a text index with CREATE TEXT INDEX (or client.create_text_index()) +# and drop it in the finally block. Tests that deliberately cover the "no-index" +# case (test_text_search_empty_for_unindexed_label) must NOT create an index. def _fts(fn): """Wrap an FTS test to handle servers without TextService. @@ -606,6 +604,7 @@ def test_text_search_returns_results(client): """text_search() finds nodes whose text property matches the query.""" label = f"FtsTest_{uid()}" tag = uid() + idx_name = f"idx_{label.lower()}" # CoordiNode executor serialises a node variable as Value::Int(node_id) — runner.rs NodeScan # path. No id() function needed; rows[0]["node_id"] is the integer internal node id. rows = client.cypher( @@ -613,11 +612,14 @@ def test_text_search_returns_results(client): params={"tag": tag}, ) seed_id = rows[0]["node_id"] + # Text index must be created explicitly; nodes written before index creation + # are indexed immediately at DDL time. + idx_info = client.create_text_index(idx_name, label, "body") + assert isinstance(idx_info, TextIndexInfo) try: results = client.text_search(label, "machine learning", limit=5) assert isinstance(results, list) - if not results: - pytest.xfail("text_search returned no results — FTS index not available on this server") + assert results, "text_search returned no results after index creation" assert any(r.node_id == seed_id for r in results), ( f"seeded node {seed_id} not found in text_search results: {results}" ) @@ -628,6 +630,7 @@ def test_text_search_returns_results(client): assert r.score > 0 assert isinstance(r.snippet, str) finally: + client.drop_text_index(idx_name) client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @@ -663,17 +666,19 @@ def test_text_search_fuzzy(client): """text_search() with fuzzy=True matches approximate terms.""" label = f"FtsFuzzyTest_{uid()}" tag = uid() + idx_name = f"idx_{label.lower()}" client.cypher( f"CREATE (n:{label} {{tag: $tag, body: 'coordinode graph database'}})", params={"tag": tag}, ) + client.create_text_index(idx_name, label, "body") try: - # "coordinode" with a typo — fuzzy should still match + # "coordinode" with a one-character typo — Levenshtein-1 fuzzy must match. results = client.text_search(label, "coordinod", fuzzy=True, limit=5) assert isinstance(results, list) - # May return 0 results if fuzzy is not yet supported or index is cold; - # just verify the call does not raise. + assert results, "fuzzy text_search returned no results after index creation" finally: + client.drop_text_index(idx_name) client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @@ -682,6 +687,7 @@ def test_hybrid_text_vector_search_returns_results(client): """hybrid_text_vector_search() returns HybridResult list with RRF scores.""" label = f"FtsHybridTest_{uid()}" tag = uid() + idx_name = f"idx_{label.lower()}" vec = [float(i) / 16 for i in range(16)] # Same node-as-int pattern: RETURN n → Value::Int(node_id) in CoordiNode executor. rows = client.cypher( @@ -689,6 +695,7 @@ def test_hybrid_text_vector_search_returns_results(client): params={"tag": tag, "vec": vec}, ) seed_id = rows[0]["node_id"] + client.create_text_index(idx_name, label, "body") try: results = client.hybrid_text_vector_search( label, @@ -698,7 +705,7 @@ def test_hybrid_text_vector_search_returns_results(client): ) assert isinstance(results, list) if not results: - pytest.xfail("hybrid_text_vector_search returned no results — FTS index not available on this server") + pytest.xfail("hybrid_text_vector_search returned no results — vector index not available on this server") assert any(r.node_id == seed_id for r in results), ( f"seeded node {seed_id} not found in hybrid_text_vector_search results: {results}" ) @@ -708,4 +715,5 @@ def test_hybrid_text_vector_search_returns_results(client): assert isinstance(r.score, float) assert r.score > 0 finally: + client.drop_text_index(idx_name) client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DETACH DELETE n", params={"tag": tag}) From b74f6eb2712a51800c0d9b89c5d153f12acd60bf Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:57:51 +0300 Subject: [PATCH 58/86] fix(tests): replace DETACH DELETE with two-step edge+node cleanup DETACH DELETE may not reliably remove connected edges when nodes have relationships. Use explicit MATCH-DELETE for edges then nodes to ensure clean test isolation. --- tests/integration/test_sdk.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 1c75da8..610e8ef 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -541,7 +541,11 @@ def test_create_edge_type_appears_in_get_edge_types(client): names = [et.name for et in edge_types] assert name in names, f"{name} not in {names}" finally: - client.cypher("MATCH (n:VisibleEtNode {tag: $tag}) DETACH DELETE n", params={"tag": tag}) + client.cypher( + "MATCH (n:VisibleEtNode {tag: $tag})-[r]-() DELETE r", + params={"tag": tag}, + ) + client.cypher("MATCH (n:VisibleEtNode {tag: $tag}) DELETE n", params={"tag": tag}) # ── Vector search ───────────────────────────────────────────────────────────── From 0c677c9f2866df6ce197dc9831945f8cd53d323d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 02:59:17 +0300 Subject: [PATCH 59/86] docs(demo): clarify embedded vs server version in README note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colab notebooks use coordinode-embedded bundling coordinode-rs v0.3.13. Docker Compose stack uses the CoordiNode server image v0.3.14. These are separate version lines — make the distinction explicit. --- demo/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/README.md b/demo/README.md index 62a10df..e7b0558 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,7 +13,8 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. -> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.13. +> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.13 (embedded engine used in Colab). +> The Docker Compose stack below uses the CoordiNode **server** image v0.3.14. ## Run locally (Docker Compose) From c0499661b4e96b708cd4063b8fc5c6d527fb718d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:13:47 +0300 Subject: [PATCH 60/86] build(deps): bump coordinode-rs submodule to v0.3.14 (5fc543b) Updates embedded engine to include all fixes after v0.3.13: - percentileCont/percentileDisc functions - Documentation and API reference improvements Aligns embedded engine with CoordiNode server image v0.3.14. --- coordinode-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinode-rs b/coordinode-rs index 8dc198f..5fc543b 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 8dc198f7e00d6f363edd9a6dce4695bbea23c9a1 +Subproject commit 5fc543b04f0a732ec394eae84b130f4542754378 From e3ff279645b603fc80cae4412c38077a8be0f2ac Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:19:59 +0300 Subject: [PATCH 61/86] refactor(client): extract _validate_property_dict helper, add Cypher identifier validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract per-property validation into _validate_property_dict static method, reducing _build_property_definitions cognitive complexity from 16 → ~10 (fixes SonarCloud threshold of 15) - Add _CYPHER_IDENT_RE regex and _validate_cypher_identifier() module helper; validate name, label, and each property in create_text_index(), and name in drop_text_index() before interpolating into DDL Cypher strings - Add missing port 37084 (metrics/health) to demo/README.md port table to match docker-compose.yml --- coordinode/coordinode/client.py | 58 ++++++++++++++++++++++++--------- demo/README.md | 1 + 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 42b20a2..690d9f1 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -28,6 +28,21 @@ # be reliably distinguished from a "host:port" pair. _HOST_PORT_RE = re.compile(r"^(\[.+\]|[^:]+):(\d+)$") +# Cypher identifier: must start with a letter or underscore, followed by +# letters, digits, or underscores. Validated before interpolating user-supplied +# names/labels/properties into DDL strings to surface clear errors early. +_CYPHER_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +def _validate_cypher_identifier(value: str, param_name: str) -> None: + """Raise :exc:`ValueError` if *value* is not a valid Cypher identifier.""" + if not isinstance(value, str) or not _CYPHER_IDENT_RE.match(value): + raise ValueError( + f"{param_name} must be a valid Cypher identifier (letters, digits, underscores, " + f"starting with a letter or underscore); got {value!r}" + ) + + # ── Low-level helpers ──────────────────────────────────────────────────────── @@ -411,6 +426,27 @@ async def get_edge_types(self) -> list[EdgeTypeInfo]: resp = await self._schema_stub.ListEdgeTypes(ListEdgeTypesRequest(), timeout=self._timeout) return [EdgeTypeInfo(et) for et in resp.edge_types] + @staticmethod + def _validate_property_dict(p: Any, idx: int) -> tuple[str, str, bool, bool]: + """Validate a single property dict and return ``(name, type_str, required, unique)``.""" + if not isinstance(p, dict): + raise ValueError(f"Property at index {idx} must be a dict; got {p!r}") + name = p.get("name") + if not isinstance(name, str) or not name: + raise ValueError(f"Property at index {idx} must have a non-empty 'name' key; got {p!r}") + raw_type = p.get("type", "string") + if "type" in p and not isinstance(raw_type, str): + raise ValueError(f"Property {name!r} must use a string value for 'type'; got {raw_type!r}") + type_str = str(raw_type).strip().lower() + required = p.get("required", False) + unique = p.get("unique", False) + if not isinstance(required, bool) or not isinstance(unique, bool): + raise ValueError( + f"Property {name!r} must use boolean values for 'required' and 'unique'; got " + f"required={required!r}, unique={unique!r}" + ) + return name, type_str, required, unique + @staticmethod def _build_property_definitions( properties: list[dict[str, Any]] | None, @@ -439,27 +475,12 @@ def _build_property_definitions( raise ValueError(f"'properties' must be a list of property dicts or None; got {type(properties).__name__}") result = [] for idx, p in enumerate(properties): - if not isinstance(p, dict): - raise ValueError(f"Property at index {idx} must be a dict; got {p!r}") - name = p.get("name") - if not isinstance(name, str) or not name: - raise ValueError(f"Property at index {idx} must have a non-empty 'name' key; got {p!r}") - raw_type = p.get("type", "string") - if "type" in p and not isinstance(raw_type, str): - raise ValueError(f"Property {name!r} must use a string value for 'type'; got {raw_type!r}") - type_str = str(raw_type).strip().lower() + name, type_str, required, unique = AsyncCoordinodeClient._validate_property_dict(p, idx) if type_str not in type_map: raise ValueError( f"Unknown property type {type_str!r} for property {name!r}. " f"Expected 'type' to be one of: {sorted(type_map)}" ) - required = p.get("required", False) - unique = p.get("unique", False) - if not isinstance(required, bool) or not isinstance(unique, bool): - raise ValueError( - f"Property {name!r} must use boolean values for 'required' and 'unique'; got " - f"required={required!r}, unique={unique!r}" - ) result.append( property_definition_cls( name=name, @@ -568,10 +589,14 @@ async def create_text_index( info = await client.create_text_index("article_body", "Article", "body") # then: results = await client.text_search("Article", "machine learning") """ + _validate_cypher_identifier(name, "name") + _validate_cypher_identifier(label, "label") if isinstance(properties, str): prop_list = [properties] else: prop_list = list(properties) + for prop in prop_list: + _validate_cypher_identifier(prop, "property") props_expr = ", ".join(prop_list) lang_clause = f" DEFAULT LANGUAGE {language}" if language else "" cypher = f"CREATE TEXT INDEX {name} ON :{label}({props_expr}){lang_clause}" @@ -590,6 +615,7 @@ async def drop_text_index(self, name: str) -> None: await client.drop_text_index("article_body") """ + _validate_cypher_identifier(name, "name") await self.cypher(f"DROP TEXT INDEX {name}") async def traverse( diff --git a/demo/README.md b/demo/README.md index e7b0558..7e91664 100644 --- a/demo/README.md +++ b/demo/README.md @@ -30,6 +30,7 @@ Open: http://localhost:38888 (token: `demo`) | Port | Service | |------|---------| | 37080 | CoordiNode gRPC | +| 37084 | CoordiNode metrics/health (`/metrics`, `/health`) | | 38888 | Jupyter Lab | ## With OpenAI (optional) From 0e2fde188fb0adc5597a956cd3aa9fb7055fe0f4 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:21:58 +0300 Subject: [PATCH 62/86] build(demo): bump notebook pin to e3ff279 (coordinode-rs v0.3.14) Update @b23c833 install reference in all 4 Colab notebooks to @e3ff279, which includes the coordinode-rs v0.3.14 submodule; update README note from v0.3.13 to v0.3.14. --- demo/README.md | 2 +- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 4 ++-- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/README.md b/demo/README.md index 7e91664..3ab198b 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,7 +13,7 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. -> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.13 (embedded engine used in Colab). +> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.14 (embedded engine used in Colab). > The Docker Compose stack below uses the CoordiNode **server** image v0.3.14. ## Run locally (Docker Compose) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index bf5c5f8..117238a 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -97,7 +97,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 9b06144..944b260 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -109,7 +109,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 5795820..8bb4392 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -106,7 +106,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 34c1f43..a214110 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -95,7 +95,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", From bd8d0dc5ec7b2a387ca85ca11d73d1dead113f71 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:25:58 +0300 Subject: [PATCH 63/86] fix(deps): point coordinode-rs submodule to published v0.3.14 tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous pointer (5fc543b) was a local-only commit not pushed to structured-world/coordinode, causing CI submodule fetch to fail. Switch to 4c80fbe — the official release commit tagged v0.3.14 that exists on GitHub. --- coordinode-rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinode-rs b/coordinode-rs index 5fc543b..4c80fbe 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 5fc543b04f0a732ec394eae84b130f4542754378 +Subproject commit 4c80fbef57eeee16efa471dc544704d592972b4a From 7534b1415538a08a552441390efd489ab361e42e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:26:49 +0300 Subject: [PATCH 64/86] build(demo): update notebook pin to bd8d0dc (correct v0.3.14 submodule) --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 4 ++-- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 117238a..7bf9cd8 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -97,7 +97,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 944b260..345c255 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -109,7 +109,7 @@ "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 8bb4392..0b65653 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -106,7 +106,7 @@ "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index a214110..044350f 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -95,7 +95,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@e3ff279#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", From b6ae1c5e72050ec8cfd5437201a31256cdaebfb3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:55:25 +0300 Subject: [PATCH 65/86] fix(client): validate empty properties list and language identifier in create_text_index - raise ValueError when properties list is empty - validate language as Cypher identifier before DDL interpolation - replace two-step edge+node DELETE with DETACH DELETE in integration test (undirected MATCH returns each edge twice, causing duplicate DELETE failure) fix(notebooks): skip embedded install when COORDINODE_ADDR is set - 01_llama, 02_langchain: guard Rust build with COORDINODE_ADDR check so Docker/server stacks skip the redundant ~5 min Colab build step - 03_langgraph: use full uuid4().hex (32 chars) for session ID to avoid collision in concurrent demo sessions - 03_langgraph: auto-append LIMIT 20 in query_facts when query has no LIMIT clause, bounding LLM-generated query result size --- coordinode/coordinode/client.py | 4 + .../01_llama_index_property_graph.ipynb | 6 +- demo/notebooks/02_langchain_graph_chain.ipynb | 11 +-- demo/notebooks/03_langgraph_agent.ipynb | 91 +------------------ tests/integration/test_sdk.py | 3 +- 5 files changed, 15 insertions(+), 100 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 690d9f1..188c00c 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -595,8 +595,12 @@ async def create_text_index( prop_list = [properties] else: prop_list = list(properties) + if not prop_list: + raise ValueError("'properties' must contain at least one property name") for prop in prop_list: _validate_cypher_identifier(prop, "property") + if language: + _validate_cypher_identifier(language, "language") props_expr = ", ".join(prop_list) lang_clause = f" DEFAULT LANGUAGE {language}" if language else "" cypher = f"CREATE TEXT INDEX {name} ON :{label}({props_expr}){lang_clause}" diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 345c255..a6394c7 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -39,7 +39,9 @@ "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + ] }, { "cell_type": "markdown", @@ -318,4 +320,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 0b65653..2cfe63c 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -36,7 +36,9 @@ "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + ] }, { "cell_type": "markdown", @@ -277,10 +279,7 @@ "metadata": {}, "outputs": [], "source": [ - "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\n", - "print(\"Cleaned up\")\n", - "graph.close()\n", - "client.close() # injected client — owned by caller" + "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n # atomically removes edges before deleting node\", params={\"tag\": tag})\nprint(\"Cleaned up\")\ngraph.close()\nclient.close() # injected client — owned by caller" ] } ], @@ -296,4 +295,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 044350f..330dce0 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -121,96 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "import os, re, uuid\n", - "from langchain_core.tools import tool\n", - "\n", - "SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n", - "\n", - "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", - "# Regex guards for query_facts (demo safety guard).\n", - "_WRITE_CLAUSE_RE = re.compile(\n", - " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", - " re.IGNORECASE | re.DOTALL,\n", - ")\n", - "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", - "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", - "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", - "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", - "# In production code, use server-side row-level security instead of client regex.\n", - "_SESSION_WHERE_SCOPE_RE = re.compile(\n", - " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n", - " re.IGNORECASE | re.DOTALL,\n", - ")\n", - "_SESSION_NODE_SCOPE_RE = re.compile(\n", - " r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n", - " re.IGNORECASE | re.DOTALL,\n", - ")\n", - "\n", - "\n", - "@tool\n", - "def save_fact(subject: str, relation: str, obj: str) -> str:\n", - " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", - " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", - " rel_type = relation.upper().replace(\" \", \"_\")\n", - " # Validate rel_type before interpolating into Cypher to prevent injection.\n", - " if not _REL_TYPE_RE.fullmatch(rel_type):\n", - " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", - " client.cypher(\n", - " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", - " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", - " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", - " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", - " )\n", - " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", - "\n", - "\n", - "@tool\n", - "def query_facts(cypher: str) -> str:\n", - " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", - " Must scope reads via either WHERE .session = $sess\n", - " or a node pattern {session: $sess}.\"\"\"\n", - " q = cypher.strip()\n", - " if _WRITE_CLAUSE_RE.search(q):\n", - " return \"Only read-only Cypher is allowed in query_facts.\"\n", - " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", - " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", - " if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n", - " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", - " rows = client.cypher(q, params={\"sess\": SESSION})\n", - " return str(rows[:20]) if rows else \"No results\"\n", - "\n", - "\n", - "@tool\n", - "def find_related(entity_name: str, depth: int = 1) -> str:\n", - " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", - " safe_depth = max(1, min(int(depth), 3))\n", - " rows = client.cypher(\n", - " f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", - " \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n", - " \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n", - " params={\"name\": entity_name, \"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return f\"No related entities found for {entity_name}\"\n", - " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", - "\n", - "\n", - "@tool\n", - "def list_all_facts() -> str:\n", - " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", - " rows = client.cypher(\n", - " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", - " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", - " params={\"sess\": SESSION},\n", - " )\n", - " if not rows:\n", - " return \"No facts stored yet\"\n", - " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", - "\n", - "\n", - "tools = [save_fact, query_facts, find_related, list_all_facts]\n", - "print(f\"Session: {SESSION}\")\n", - "print(\"Tools:\", [t.name for t in tools])" + "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n # Enforce a result cap so agents cannot dump the entire graph.\n _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+\\d+\\b\", re.IGNORECASE)\n if not _LIMIT_RE.search(q):\n q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" ] }, { diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 610e8ef..a93acdd 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -542,10 +542,9 @@ def test_create_edge_type_appears_in_get_edge_types(client): assert name in names, f"{name} not in {names}" finally: client.cypher( - "MATCH (n:VisibleEtNode {tag: $tag})-[r]-() DELETE r", + "MATCH (n:VisibleEtNode {tag: $tag}) DETACH DELETE n", params={"tag": tag}, ) - client.cypher("MATCH (n:VisibleEtNode {tag: $tag}) DELETE n", params={"tag": tag}) # ── Vector search ───────────────────────────────────────────────────────────── From 01818942b3b6714341570525a4f33983e189cc9c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 03:57:05 +0300 Subject: [PATCH 66/86] docs(notebooks): add DETACH DELETE rationale in langchain cleanup cell --- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 2cfe63c..d7e915d 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -279,7 +279,7 @@ "metadata": {}, "outputs": [], "source": [ - "graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n # atomically removes edges before deleting node\", params={\"tag\": tag})\nprint(\"Cleaned up\")\ngraph.close()\nclient.close() # injected client — owned by caller" + "# DETACH DELETE atomically removes all edges then the node in one operation.\n# Two-step MATCH (n)-[r]-() / DELETE r / DELETE n is avoided because an\n# undirected MATCH returns each edge from both endpoints, so the second pass\n# fails with \"cannot delete node with connected edges\".\ngraph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\nprint(\"Cleaned up\")\ngraph.close()\nclient.close() # injected client — owned by caller" ] } ], From d75026e52d66058dbead66e30b3a3de11ed96c14 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 08:52:52 +0300 Subject: [PATCH 67/86] fix(notebooks): use full UUID for run tags; drop stub schema methods from embedded adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01_llama, 02_langchain: use full uuid4().hex for `tag` variable (was [:6] = 24 bits entropy, risked cross-run collisions on shared envs) - 01_llama, 02_langchain: remove get_labels()/get_edge_types() from _EmbeddedAdapter — returning [] made adapter look schema-RPC-capable, silently short-circuiting the get_schema_text() path in CoordinodeGraph - 03_langgraph: replace explicit LIMIT N > 20 in query_facts via re.sub instead of only appending when absent; server no longer materializes oversized result sets from LLM-generated queries fix(client): remove contradictory "indexed automatically" claim from text_search() Args.label docstring; Note already says indexing requires explicit CREATE TEXT INDEX DDL --- coordinode/coordinode/client.py | 5 +- .../01_llama_index_property_graph.ipynb | 1406 +++++++++++- demo/notebooks/02_langchain_graph_chain.ipynb | 1897 ++++++++++++++++- demo/notebooks/03_langgraph_agent.ipynb | 2 +- 4 files changed, 3201 insertions(+), 109 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 188c00c..0d6e5cb 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -686,10 +686,7 @@ async def text_search( """Run a full-text BM25 search over all indexed text properties for *label*. Args: - label: Node label to search (e.g. ``"Article"``). On current - CoordiNode servers, text properties in schema-free graphs are - indexed automatically; labels with no searchable text content - simply return ``[]``. + label: Node label to search (e.g. ``"Article"``). query: Full-text query string. Supports boolean operators (``AND``, ``OR``, ``NOT``), phrase search (``"exact phrase"``), prefix wildcards (``term*``), and per-term boosting (``term^N``). diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index a6394c7..aebb719 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -62,39 +62,7 @@ "metadata": {}, "outputs": [], "source": [ - "class _EmbeddedAdapter:\n", - " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", - "\n", - " def __init__(self, local_client):\n", - " self._lc = local_client\n", - "\n", - " def cypher(self, query, params=None):\n", - " return self._lc.cypher(query, params or {})\n", - "\n", - " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", - " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", - " lines = [\"Node labels:\"]\n", - " for r in lbls:\n", - " lines.append(f\" - {r['lbl']}\")\n", - " lines.append(\"\\nEdge types:\")\n", - " for r in rels:\n", - " lines.append(f\" - {r['t']}\")\n", - " return \"\\n\".join(lines)\n", - "\n", - " # Vector search not available in embedded mode — requires running CoordiNode server.\n", - "\n", - " def close(self):\n", - " self._lc.close()\n", - "\n", - " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n", - "\n", - " return []\n", - "\n", - " def get_edge_types(self):\n", - " # Schema introspection not available in embedded mode.\n", - " return []" + "class _EmbeddedAdapter:\n \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n\n def __init__(self, local_client):\n self._lc = local_client\n\n def cypher(self, query, params=None):\n return self._lc.cypher(query, params or {})\n\n def get_schema_text(self):\n lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n lines = [\"Node labels:\"]\n for r in lbls:\n lines.append(f\" - {r['lbl']}\")\n lines.append(\"\\nEdge types:\")\n for r in rels:\n lines.append(f\" - {r['t']}\")\n return \"\\n\".join(lines)\n\n # Vector search not available in embedded mode — requires running CoordiNode server.\n\n def close(self):\n self._lc.close()\n" ] }, { @@ -111,7 +79,1356 @@ "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n # Works without Docker or any external service; data is in-memory.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": [ + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "o", + "c", + "k", + "e", + "t", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "p", + "o", + "r", + "t", + "_", + "o", + "p", + "e", + "n", + "(", + "p", + "o", + "r", + "t", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "s", + "o", + "c", + "k", + "e", + "t", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "i", + "o", + "n", + "(", + "(", + "\"", + "1", + "2", + "7", + ".", + "0", + ".", + "0", + ".", + "1", + "\"", + ",", + " ", + "p", + "o", + "r", + "t", + ")", + ",", + " ", + "t", + "i", + "m", + "e", + "o", + "u", + "t", + "=", + "1", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "O", + "S", + "E", + "r", + "r", + "o", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "\n", + "\n", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + " ", + "=", + " ", + "i", + "n", + "t", + "(", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "P", + "O", + "R", + "T", + "\"", + ",", + " ", + "\"", + "7", + "0", + "8", + "0", + "\"", + ")", + ")", + "\n", + "\n", + "i", + "f", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "\"", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + " ", + "=", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "\"", + "]", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "e", + "d", + " ", + "t", + "o", + " ", + "{", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "}", + "\"", + ")", + "\n", + "e", + "l", + "i", + "f", + " ", + "_", + "p", + "o", + "r", + "t", + "_", + "o", + "p", + "e", + "n", + "(", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + " ", + "=", + " ", + "f", + "\"", + "l", + "o", + "c", + "a", + "l", + "h", + "o", + "s", + "t", + ":", + "{", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "e", + "d", + " ", + "t", + "o", + " ", + "{", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "}", + "\"", + ")", + "\n", + "e", + "l", + "s", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "N", + "o", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "—", + " ", + "u", + "s", + "e", + " ", + "t", + "h", + "e", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "n", + "-", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + " ", + "e", + "n", + "g", + "i", + "n", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "W", + "o", + "r", + "k", + "s", + " ", + "w", + "i", + "t", + "h", + "o", + "u", + "t", + " ", + "D", + "o", + "c", + "k", + "e", + "r", + " ", + "o", + "r", + " ", + "a", + "n", + "y", + " ", + "e", + "x", + "t", + "e", + "r", + "n", + "a", + "l", + " ", + "s", + "e", + "r", + "v", + "i", + "c", + "e", + ";", + " ", + "d", + "a", + "t", + "a", + " ", + "i", + "s", + " ", + "i", + "n", + "-", + "m", + "e", + "m", + "o", + "r", + "y", + ".", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "_", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "I", + "m", + "p", + "o", + "r", + "t", + "E", + "r", + "r", + "o", + "r", + " ", + "a", + "s", + " ", + "e", + "x", + "c", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "a", + "i", + "s", + "e", + " ", + "R", + "u", + "n", + "t", + "i", + "m", + "e", + "E", + "r", + "r", + "o", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "d", + ".", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "R", + "u", + "n", + ":", + " ", + "p", + "i", + "p", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "@", + "b", + "d", + "8", + "d", + "0", + "d", + "c", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + " ", + "—", + " ", + "o", + "r", + " ", + "s", + "t", + "a", + "r", + "t", + " ", + "a", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "a", + "n", + "d", + " ", + "s", + "e", + "t", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ".", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ")", + " ", + "f", + "r", + "o", + "m", + " ", + "e", + "x", + "c", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "l", + "c", + " ", + "=", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "\"", + ":", + "m", + "e", + "m", + "o", + "r", + "y", + ":", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "_", + "E", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "A", + "d", + "a", + "p", + "t", + "e", + "r", + "(", + "_", + "l", + "c", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "U", + "s", + "i", + "n", + "g", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + " ", + "(", + "i", + "n", + "-", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ")", + "\"", + ")" + ] }, { "cell_type": "markdown", @@ -155,26 +1472,7 @@ "metadata": {}, "outputs": [], "source": [ - "import uuid\n", - "\n", - "tag = uuid.uuid4().hex[:6]\n", - "\n", - "nodes = [\n", - " EntityNode(label=\"Person\", name=f\"Alice-{tag}\", properties={\"role\": \"researcher\", \"field\": \"AI\"}),\n", - " EntityNode(label=\"Person\", name=f\"Bob-{tag}\", properties={\"role\": \"engineer\", \"field\": \"ML\"}),\n", - " EntityNode(label=\"Topic\", name=f\"GraphRAG-{tag}\", properties={\"domain\": \"knowledge graphs\"}),\n", - "]\n", - "store.upsert_nodes(nodes)\n", - "print(\"Upserted nodes:\", [n.name for n in nodes])\n", - "\n", - "alice, bob, graphrag = nodes\n", - "relations = [\n", - " Relation(label=\"RESEARCHES\", source_id=alice.id, target_id=graphrag.id, properties={\"since\": 2023}),\n", - " Relation(label=\"COLLABORATES\", source_id=alice.id, target_id=bob.id),\n", - " Relation(label=\"IMPLEMENTS\", source_id=bob.id, target_id=graphrag.id),\n", - "]\n", - "store.upsert_relations(relations)\n", - "print(\"Upserted relations:\", [r.label for r in relations])" + "import uuid\n\ntag = uuid.uuid4().hex\n\nnodes = [\n EntityNode(label=\"Person\", name=f\"Alice-{tag}\", properties={\"role\": \"researcher\", \"field\": \"AI\"}),\n EntityNode(label=\"Person\", name=f\"Bob-{tag}\", properties={\"role\": \"engineer\", \"field\": \"ML\"}),\n EntityNode(label=\"Topic\", name=f\"GraphRAG-{tag}\", properties={\"domain\": \"knowledge graphs\"}),\n]\nstore.upsert_nodes(nodes)\nprint(\"Upserted nodes:\", [n.name for n in nodes])\n\nalice, bob, graphrag = nodes\nrelations = [\n Relation(label=\"RESEARCHES\", source_id=alice.id, target_id=graphrag.id, properties={\"since\": 2023}),\n Relation(label=\"COLLABORATES\", source_id=alice.id, target_id=bob.id),\n Relation(label=\"IMPLEMENTS\", source_id=bob.id, target_id=graphrag.id),\n]\nstore.upsert_relations(relations)\nprint(\"Upserted relations:\", [r.label for r in relations])" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index d7e915d..5c57fed 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -59,39 +59,7 @@ "metadata": {}, "outputs": [], "source": [ - "class _EmbeddedAdapter:\n", - " \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n", - "\n", - " def __init__(self, local_client):\n", - " self._lc = local_client\n", - "\n", - " def cypher(self, query, params=None):\n", - " return self._lc.cypher(query, params or {})\n", - "\n", - " def get_schema_text(self):\n", - " lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n", - " rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n", - " lines = [\"Node labels:\"]\n", - " for r in lbls:\n", - " lines.append(f\" - {r['lbl']}\")\n", - " lines.append(\"\\nEdge types:\")\n", - " for r in rels:\n", - " lines.append(f\" - {r['t']}\")\n", - " return \"\\n\".join(lines)\n", - "\n", - " # Vector search not available in embedded mode — requires running CoordiNode server.\n", - "\n", - " def close(self):\n", - " self._lc.close()\n", - "\n", - " def get_labels(self):\n", - " # Schema introspection not available in embedded mode.\n", - "\n", - " return []\n", - "\n", - " def get_edge_types(self):\n", - " # Schema introspection not available in embedded mode.\n", - " return []" + "class _EmbeddedAdapter:\n \"\"\"Thin wrapper around LocalClient that adds CoordinodeClient-compatible methods.\"\"\"\n\n def __init__(self, local_client):\n self._lc = local_client\n\n def cypher(self, query, params=None):\n return self._lc.cypher(query, params or {})\n\n def get_schema_text(self):\n lbls = self._lc.cypher(\"MATCH (n) UNWIND labels(n) AS lbl RETURN DISTINCT lbl ORDER BY lbl\")\n rels = self._lc.cypher(\"MATCH ()-[r]->() RETURN DISTINCT type(r) AS t ORDER BY t\")\n lines = [\"Node labels:\"]\n for r in lbls:\n lines.append(f\" - {r['lbl']}\")\n lines.append(\"\\nEdge types:\")\n for r in rels:\n lines.append(f\" - {r['t']}\")\n return \"\\n\".join(lines)\n\n # Vector search not available in embedded mode — requires running CoordiNode server.\n\n def close(self):\n self._lc.close()\n" ] }, { @@ -108,7 +76,1285 @@ "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import os, socket\n\n\ndef _port_open(port):\n try:\n with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n return True\n except OSError:\n return False\n\n\nGRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n\nif os.environ.get(\"COORDINODE_ADDR\"):\n COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelif _port_open(GRPC_PORT):\n COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n from coordinode import CoordinodeClient\n\n client = CoordinodeClient(COORDINODE_ADDR)\n print(f\"Connected to {COORDINODE_ADDR}\")\nelse:\n # No server available — use the embedded in-process engine.\n try:\n from coordinode_embedded import LocalClient\n except ImportError as exc:\n raise RuntimeError(\n \"coordinode-embedded is not installed. \"\n \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n ) from exc\n\n _lc = LocalClient(\":memory:\")\n client = _EmbeddedAdapter(_lc)\n print(\"Using embedded LocalClient (in-process)\")" + "source": [ + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "o", + "s", + ",", + " ", + "s", + "o", + "c", + "k", + "e", + "t", + "\n", + "\n", + "\n", + "d", + "e", + "f", + " ", + "_", + "p", + "o", + "r", + "t", + "_", + "o", + "p", + "e", + "n", + "(", + "p", + "o", + "r", + "t", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "w", + "i", + "t", + "h", + " ", + "s", + "o", + "c", + "k", + "e", + "t", + ".", + "c", + "r", + "e", + "a", + "t", + "e", + "_", + "c", + "o", + "n", + "n", + "e", + "c", + "t", + "i", + "o", + "n", + "(", + "(", + "\"", + "1", + "2", + "7", + ".", + "0", + ".", + "0", + ".", + "1", + "\"", + ",", + " ", + "p", + "o", + "r", + "t", + ")", + ",", + " ", + "t", + "i", + "m", + "e", + "o", + "u", + "t", + "=", + "1", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "T", + "r", + "u", + "e", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "O", + "S", + "E", + "r", + "r", + "o", + "r", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "e", + "t", + "u", + "r", + "n", + " ", + "F", + "a", + "l", + "s", + "e", + "\n", + "\n", + "\n", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + " ", + "=", + " ", + "i", + "n", + "t", + "(", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "P", + "O", + "R", + "T", + "\"", + ",", + " ", + "\"", + "7", + "0", + "8", + "0", + "\"", + ")", + ")", + "\n", + "\n", + "i", + "f", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "\"", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + " ", + "=", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "\"", + "]", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "e", + "d", + " ", + "t", + "o", + " ", + "{", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "}", + "\"", + ")", + "\n", + "e", + "l", + "i", + "f", + " ", + "_", + "p", + "o", + "r", + "t", + "_", + "o", + "p", + "e", + "n", + "(", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + " ", + "=", + " ", + "f", + "\"", + "l", + "o", + "c", + "a", + "l", + "h", + "o", + "s", + "t", + ":", + "{", + "G", + "R", + "P", + "C", + "_", + "P", + "O", + "R", + "T", + "}", + "\"", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "f", + "\"", + "C", + "o", + "n", + "n", + "e", + "c", + "t", + "e", + "d", + " ", + "t", + "o", + " ", + "{", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + "}", + "\"", + ")", + "\n", + "e", + "l", + "s", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + "#", + " ", + "N", + "o", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "a", + "v", + "a", + "i", + "l", + "a", + "b", + "l", + "e", + " ", + "—", + " ", + "u", + "s", + "e", + " ", + "t", + "h", + "e", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "n", + "-", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + " ", + "e", + "n", + "g", + "i", + "n", + "e", + ".", + "\n", + " ", + " ", + " ", + " ", + "t", + "r", + "y", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "_", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + "\n", + " ", + " ", + " ", + " ", + "e", + "x", + "c", + "e", + "p", + "t", + " ", + "I", + "m", + "p", + "o", + "r", + "t", + "E", + "r", + "r", + "o", + "r", + " ", + "a", + "s", + " ", + "e", + "x", + "c", + ":", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "r", + "a", + "i", + "s", + "e", + " ", + "R", + "u", + "n", + "t", + "i", + "m", + "e", + "E", + "r", + "r", + "o", + "r", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + "e", + "d", + ".", + " ", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + "R", + "u", + "n", + ":", + " ", + "p", + "i", + "p", + " ", + "i", + "n", + "s", + "t", + "a", + "l", + "l", + " ", + "g", + "i", + "t", + "+", + "h", + "t", + "t", + "p", + "s", + ":", + "/", + "/", + "g", + "i", + "t", + "h", + "u", + "b", + ".", + "c", + "o", + "m", + "/", + "s", + "t", + "r", + "u", + "c", + "t", + "u", + "r", + "e", + "d", + "-", + "w", + "o", + "r", + "l", + "d", + "/", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "p", + "y", + "t", + "h", + "o", + "n", + ".", + "g", + "i", + "t", + "@", + "b", + "d", + "8", + "d", + "0", + "d", + "c", + "#", + "s", + "u", + "b", + "d", + "i", + "r", + "e", + "c", + "t", + "o", + "r", + "y", + "=", + "c", + "o", + "o", + "r", + "d", + "i", + "n", + "o", + "d", + "e", + "-", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "\"", + " ", + "—", + " ", + "o", + "r", + " ", + "s", + "t", + "a", + "r", + "t", + " ", + "a", + " ", + "C", + "o", + "o", + "r", + "d", + "i", + "N", + "o", + "d", + "e", + " ", + "s", + "e", + "r", + "v", + "e", + "r", + " ", + "a", + "n", + "d", + " ", + "s", + "e", + "t", + " ", + "C", + "O", + "O", + "R", + "D", + "I", + "N", + "O", + "D", + "E", + "_", + "A", + "D", + "D", + "R", + ".", + "\"", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ")", + " ", + "f", + "r", + "o", + "m", + " ", + "e", + "x", + "c", + "\n", + "\n", + " ", + " ", + " ", + " ", + "_", + "l", + "c", + " ", + "=", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + "(", + "\"", + ":", + "m", + "e", + "m", + "o", + "r", + "y", + ":", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "c", + "l", + "i", + "e", + "n", + "t", + " ", + "=", + " ", + "_", + "E", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + "A", + "d", + "a", + "p", + "t", + "e", + "r", + "(", + "_", + "l", + "c", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "U", + "s", + "i", + "n", + "g", + " ", + "e", + "m", + "b", + "e", + "d", + "d", + "e", + "d", + " ", + "L", + "o", + "c", + "a", + "l", + "C", + "l", + "i", + "e", + "n", + "t", + " ", + "(", + "i", + "n", + "-", + "p", + "r", + "o", + "c", + "e", + "s", + "s", + ")", + "\"", + ")" + ] }, { "cell_type": "markdown", @@ -155,21 +1401,7 @@ "metadata": {}, "outputs": [], "source": [ - "tag = uuid.uuid4().hex[:6]\n", - "\n", - "nodes = [\n", - " Node(id=f\"Turing-{tag}\", type=\"Scientist\", properties={\"born\": 1912, \"field\": \"computer science\"}),\n", - " Node(id=f\"Shannon-{tag}\", type=\"Scientist\", properties={\"born\": 1916, \"field\": \"information theory\"}),\n", - " Node(id=f\"Cryptography-{tag}\", type=\"Field\", properties={\"era\": \"modern\"}),\n", - "]\n", - "rels = [\n", - " Relationship(source=nodes[0], target=nodes[2], type=\"FOUNDED\", properties={\"year\": 1936}),\n", - " Relationship(source=nodes[1], target=nodes[2], type=\"CONTRIBUTED_TO\"),\n", - " Relationship(source=nodes[0], target=nodes[1], type=\"INFLUENCED\"),\n", - "]\n", - "doc = GraphDocument(nodes=nodes, relationships=rels, source=Document(page_content=\"Turing and Shannon\"))\n", - "graph.add_graph_documents([doc])\n", - "print(\"Documents added\")" + "tag = uuid.uuid4().hex\n\nnodes = [\n Node(id=f\"Turing-{tag}\", type=\"Scientist\", properties={\"born\": 1912, \"field\": \"computer science\"}),\n Node(id=f\"Shannon-{tag}\", type=\"Scientist\", properties={\"born\": 1916, \"field\": \"information theory\"}),\n Node(id=f\"Cryptography-{tag}\", type=\"Field\", properties={\"era\": \"modern\"}),\n]\nrels = [\n Relationship(source=nodes[0], target=nodes[2], type=\"FOUNDED\", properties={\"year\": 1936}),\n Relationship(source=nodes[1], target=nodes[2], type=\"CONTRIBUTED_TO\"),\n Relationship(source=nodes[0], target=nodes[1], type=\"INFLUENCED\"),\n]\ndoc = GraphDocument(nodes=nodes, relationships=rels, source=Document(page_content=\"Turing and Shannon\"))\ngraph.add_graph_documents([doc])\nprint(\"Documents added\")" ] }, { @@ -262,7 +1494,572 @@ "id": "c3d4e5f6-0002-0000-0000-000000000019", "metadata": {}, "outputs": [], - "source": "if not os.environ.get(\"OPENAI_API_KEY\"):\n print(\n 'Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.'\n )\nelse:\n from langchain.chains import GraphCypherQAChain\n from langchain_openai import ChatOpenAI\n\n chain = GraphCypherQAChain.from_llm(\n ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n graph=graph,\n verbose=True,\n allow_dangerous_requests=True,\n )\n result = chain.invoke(f\"Who influenced Shannon-{tag}?\")\n print(\"Answer:\", result[\"result\"])" + "source": [ + "i", + "f", + " ", + "n", + "o", + "t", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + ".", + "g", + "e", + "t", + "(", + "\"", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + "\"", + ")", + ":", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "'", + "S", + "k", + "i", + "p", + "p", + "i", + "n", + "g", + ":", + " ", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + " ", + "i", + "s", + " ", + "n", + "o", + "t", + " ", + "s", + "e", + "t", + ".", + " ", + "S", + "e", + "t", + " ", + "i", + "t", + " ", + "v", + "i", + "a", + " ", + "o", + "s", + ".", + "e", + "n", + "v", + "i", + "r", + "o", + "n", + "[", + "\"", + "O", + "P", + "E", + "N", + "A", + "I", + "_", + "A", + "P", + "I", + "_", + "K", + "E", + "Y", + "\"", + "]", + " ", + "=", + " ", + "\"", + "s", + "k", + "-", + ".", + ".", + ".", + "\"", + " ", + "a", + "n", + "d", + " ", + "r", + "e", + "-", + "r", + "u", + "n", + " ", + "t", + "h", + "i", + "s", + " ", + "c", + "e", + "l", + "l", + ".", + "'", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + "e", + "l", + "s", + "e", + ":", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + ".", + "c", + "h", + "a", + "i", + "n", + "s", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "G", + "r", + "a", + "p", + "h", + "C", + "y", + "p", + "h", + "e", + "r", + "Q", + "A", + "C", + "h", + "a", + "i", + "n", + "\n", + " ", + " ", + " ", + " ", + "f", + "r", + "o", + "m", + " ", + "l", + "a", + "n", + "g", + "c", + "h", + "a", + "i", + "n", + "_", + "o", + "p", + "e", + "n", + "a", + "i", + " ", + "i", + "m", + "p", + "o", + "r", + "t", + " ", + "C", + "h", + "a", + "t", + "O", + "p", + "e", + "n", + "A", + "I", + "\n", + "\n", + " ", + " ", + " ", + " ", + "c", + "h", + "a", + "i", + "n", + " ", + "=", + " ", + "G", + "r", + "a", + "p", + "h", + "C", + "y", + "p", + "h", + "e", + "r", + "Q", + "A", + "C", + "h", + "a", + "i", + "n", + ".", + "f", + "r", + "o", + "m", + "_", + "l", + "l", + "m", + "(", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "C", + "h", + "a", + "t", + "O", + "p", + "e", + "n", + "A", + "I", + "(", + "m", + "o", + "d", + "e", + "l", + "=", + "\"", + "g", + "p", + "t", + "-", + "4", + "o", + "-", + "m", + "i", + "n", + "i", + "\"", + ",", + " ", + "t", + "e", + "m", + "p", + "e", + "r", + "a", + "t", + "u", + "r", + "e", + "=", + "0", + ")", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "g", + "r", + "a", + "p", + "h", + "=", + "g", + "r", + "a", + "p", + "h", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "v", + "e", + "r", + "b", + "o", + "s", + "e", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "a", + "l", + "l", + "o", + "w", + "_", + "d", + "a", + "n", + "g", + "e", + "r", + "o", + "u", + "s", + "_", + "r", + "e", + "q", + "u", + "e", + "s", + "t", + "s", + "=", + "T", + "r", + "u", + "e", + ",", + "\n", + " ", + " ", + " ", + " ", + ")", + "\n", + " ", + " ", + " ", + " ", + "r", + "e", + "s", + "u", + "l", + "t", + " ", + "=", + " ", + "c", + "h", + "a", + "i", + "n", + ".", + "i", + "n", + "v", + "o", + "k", + "e", + "(", + "f", + "\"", + "W", + "h", + "o", + " ", + "i", + "n", + "f", + "l", + "u", + "e", + "n", + "c", + "e", + "d", + " ", + "S", + "h", + "a", + "n", + "n", + "o", + "n", + "-", + "{", + "t", + "a", + "g", + "}", + "?", + "\"", + ")", + "\n", + " ", + " ", + " ", + " ", + "p", + "r", + "i", + "n", + "t", + "(", + "\"", + "A", + "n", + "s", + "w", + "e", + "r", + ":", + "\"", + ",", + " ", + "r", + "e", + "s", + "u", + "l", + "t", + "[", + "\"", + "r", + "e", + "s", + "u", + "l", + "t", + "\"", + "]", + ")" + ] }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 330dce0..64f7a22 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -121,7 +121,7 @@ "metadata": {}, "outputs": [], "source": [ - "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n # Enforce a result cap so agents cannot dump the entire graph.\n _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+\\d+\\b\", re.IGNORECASE)\n if not _LIMIT_RE.search(q):\n q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows[:20]) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" + "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n # Enforce a result cap so agents cannot dump the entire graph.\n # Cap or append LIMIT 20 before execution so the server never materializes\n # more than 20 rows, even when the LLM writes an explicit large LIMIT.\n _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+\\d+\\b\", re.IGNORECASE)\n if _LIMIT_RE.search(q):\n q = _LIMIT_RE.sub(\"LIMIT 20\", q)\n else:\n q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" ] }, { From 61eb9063b1e3d6911e8c8bca5359f6883c0470ee Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 09:47:10 +0300 Subject: [PATCH 68/86] fix(notebooks): repair JSON source format and harden notebooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Repair character-by-character cell source serialization in 01_llama (cell 6) and 02_langchain (cells 6, 18) — source must be a list of full lines, not single-char list elements - Delay COORDINODE_PORT int() parsing until the else-branch so a malformed port value does not crash startup when COORDINODE_ADDR is set - Replace two-step DELETE+DELETE with DETACH DELETE in 00_seed_data cleanup to avoid duplicate-edge errors from undirected MATCH -[r]-() - Cap explicit LIMIT to min(n, 20) in 03_langgraph query_facts instead of unconditionally replacing it, preserving LIMIT 1 and similar - Wrap create_text_index calls inside try-blocks with idx_created flag in three FTS integration tests so node cleanup always runs even when index creation fails --- demo/notebooks/00_seed_data.ipynb | 17 +- .../01_llama_index_property_graph.ipynb | 1394 +----------- demo/notebooks/02_langchain_graph_chain.ipynb | 1903 +---------------- demo/notebooks/03_langgraph_agent.ipynb | 103 +- tests/integration/test_sdk.py | 35 +- 5 files changed, 247 insertions(+), 3205 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 7bf9cd8..113cf01 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -119,7 +119,20 @@ "id": "a1b2c3d4-0000-0000-0000-000000000007", "metadata": {}, "outputs": [], - "source": "import uuid\n\nDEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\nprint(\"Using DEMO_TAG:\", DEMO_TAG)\n# Delete relationships first, then nodes — two-step ensures correct cleanup across\n# all CoordiNode versions (embedded and server).\nclient.cypher(\n \"MATCH (n {demo: true, demo_tag: $tag})-[r]-() DELETE r\",\n params={\"tag\": DEMO_TAG},\n)\nclient.cypher(\n \"MATCH (n {demo: true, demo_tag: $tag}) DELETE n\",\n params={\"tag\": DEMO_TAG},\n)\nprint(\"Previous demo data removed\")" + "source": [ + "import uuid\n", + "\n", + "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\n", + "print(\"Using DEMO_TAG:\", DEMO_TAG)\n", + "# Remove prior demo nodes and any attached relationships in one step to avoid\n", + "# duplicate relationship matches during cleanup (undirected MATCH -[r]-() returns\n", + "# each edge twice — once per endpoint — causing duplicate-delete errors).\n", + "client.cypher(\n", + " \"MATCH (n {demo: true, demo_tag: $tag}) DETACH DELETE n\",\n", + " params={\"tag\": DEMO_TAG},\n", + ")\n", + "print(\"Previous demo data removed\")" + ] }, { "cell_type": "markdown", @@ -373,4 +386,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index aebb719..27926ad 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -80,1354 +80,50 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "o", - "c", - "k", - "e", - "t", - "\n", - "\n", - "\n", - "d", - "e", - "f", - " ", - "_", - "p", - "o", - "r", - "t", - "_", - "o", - "p", - "e", - "n", - "(", - "p", - "o", - "r", - "t", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "s", - "o", - "c", - "k", - "e", - "t", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "c", - "o", - "n", - "n", - "e", - "c", - "t", - "i", - "o", - "n", - "(", - "(", - "\"", - "1", - "2", - "7", - ".", - "0", - ".", - "0", - ".", - "1", - "\"", - ",", - " ", - "p", - "o", - "r", - "t", - ")", - ",", - " ", - "t", - "i", - "m", - "e", - "o", - "u", - "t", - "=", - "1", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "T", - "r", - "u", - "e", - "\n", - " ", - " ", - " ", - " ", - "e", - "x", - "c", - "e", - "p", - "t", - " ", - "O", - "S", - "E", - "r", - "r", - "o", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "F", - "a", - "l", - "s", - "e", - "\n", - "\n", - "\n", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - " ", - "=", - " ", - "i", - "n", - "t", - "(", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "P", - "O", - "R", - "T", - "\"", - ",", - " ", - "\"", - "7", - "0", - "8", - "0", - "\"", - ")", - ")", - "\n", - "\n", - "i", - "f", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "\"", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - " ", - "=", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "\"", - "]", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "o", - "n", - "n", - "e", - "c", - "t", - "e", - "d", - " ", - "t", - "o", - " ", - "{", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "}", - "\"", - ")", - "\n", - "e", - "l", - "i", - "f", - " ", - "_", - "p", - "o", - "r", - "t", - "_", - "o", - "p", - "e", - "n", - "(", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - " ", - "=", - " ", - "f", - "\"", - "l", - "o", - "c", - "a", - "l", - "h", - "o", - "s", - "t", - ":", - "{", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "o", - "n", - "n", - "e", - "c", - "t", - "e", - "d", - " ", - "t", - "o", - " ", - "{", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "}", - "\"", - ")", - "\n", - "e", - "l", - "s", - "e", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "N", - "o", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "—", - " ", - "u", - "s", - "e", - " ", - "t", - "h", - "e", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "n", - "-", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - " ", - "e", - "n", - "g", - "i", - "n", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "W", - "o", - "r", - "k", - "s", - " ", - "w", - "i", - "t", - "h", - "o", - "u", - "t", - " ", - "D", - "o", - "c", - "k", - "e", - "r", - " ", - "o", - "r", - " ", - "a", - "n", - "y", - " ", - "e", - "x", - "t", - "e", - "r", - "n", - "a", - "l", - " ", - "s", - "e", - "r", - "v", - "i", - "c", - "e", - ";", - " ", - "d", - "a", - "t", - "a", - " ", - "i", - "s", - " ", - "i", - "n", - "-", - "m", - "e", - "m", - "o", - "r", - "y", - ".", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "_", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - " ", - " ", - " ", - " ", - "e", - "x", - "c", - "e", - "p", - "t", - " ", - "I", - "m", - "p", - "o", - "r", - "t", - "E", - "r", - "r", - "o", - "r", - " ", - "a", - "s", - " ", - "e", - "x", - "c", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "a", - "i", - "s", - "e", - " ", - "R", - "u", - "n", - "t", - "i", - "m", - "e", - "E", - "r", - "r", - "o", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "d", - ".", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "R", - "u", - "n", - ":", - " ", - "p", - "i", - "p", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "@", - "b", - "d", - "8", - "d", - "0", - "d", - "c", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - " ", - "—", - " ", - "o", - "r", - " ", - "s", - "t", - "a", - "r", - "t", - " ", - "a", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "a", - "n", - "d", - " ", - "s", - "e", - "t", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ".", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - ")", - " ", - "f", - "r", - "o", - "m", - " ", - "e", - "x", - "c", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "l", - "c", - " ", - "=", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "\"", - ":", - "m", - "e", - "m", - "o", - "r", - "y", - ":", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "_", - "E", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "A", - "d", - "a", - "p", - "t", - "e", - "r", - "(", - "_", - "l", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "U", - "s", - "i", - "n", - "g", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - " ", - "(", - "i", - "n", - "-", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ")", - "\"", - ")" + "import os, socket\n", + "\n", + "\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " try:\n", + " grpc_port = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " except ValueError as exc:\n", + " raise RuntimeError(\"COORDINODE_PORT must be an integer\") from exc\n", + "\n", + " if _port_open(grpc_port):\n", + " COORDINODE_ADDR = f\"localhost:{grpc_port}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " else:\n", + " # No server available — use the embedded in-process engine.\n", + " # Works without Docker or any external service; data is in-memory.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { @@ -1618,4 +314,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 5c57fed..8701968 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -77,1283 +77,50 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "o", - "s", - ",", - " ", - "s", - "o", - "c", - "k", - "e", - "t", - "\n", - "\n", - "\n", - "d", - "e", - "f", - " ", - "_", - "p", - "o", - "r", - "t", - "_", - "o", - "p", - "e", - "n", - "(", - "p", - "o", - "r", - "t", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "w", - "i", - "t", - "h", - " ", - "s", - "o", - "c", - "k", - "e", - "t", - ".", - "c", - "r", - "e", - "a", - "t", - "e", - "_", - "c", - "o", - "n", - "n", - "e", - "c", - "t", - "i", - "o", - "n", - "(", - "(", - "\"", - "1", - "2", - "7", - ".", - "0", - ".", - "0", - ".", - "1", - "\"", - ",", - " ", - "p", - "o", - "r", - "t", - ")", - ",", - " ", - "t", - "i", - "m", - "e", - "o", - "u", - "t", - "=", - "1", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "T", - "r", - "u", - "e", - "\n", - " ", - " ", - " ", - " ", - "e", - "x", - "c", - "e", - "p", - "t", - " ", - "O", - "S", - "E", - "r", - "r", - "o", - "r", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "e", - "t", - "u", - "r", - "n", - " ", - "F", - "a", - "l", - "s", - "e", - "\n", - "\n", - "\n", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - " ", - "=", - " ", - "i", - "n", - "t", - "(", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "P", - "O", - "R", - "T", - "\"", - ",", - " ", - "\"", - "7", - "0", - "8", - "0", - "\"", - ")", - ")", - "\n", - "\n", - "i", - "f", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "\"", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - " ", - "=", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "\"", - "]", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "o", - "n", - "n", - "e", - "c", - "t", - "e", - "d", - " ", - "t", - "o", - " ", - "{", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "}", - "\"", - ")", - "\n", - "e", - "l", - "i", - "f", - " ", - "_", - "p", - "o", - "r", - "t", - "_", - "o", - "p", - "e", - "n", - "(", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - " ", - "=", - " ", - "f", - "\"", - "l", - "o", - "c", - "a", - "l", - "h", - "o", - "s", - "t", - ":", - "{", - "G", - "R", - "P", - "C", - "_", - "P", - "O", - "R", - "T", - "}", - "\"", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "f", - "\"", - "C", - "o", - "n", - "n", - "e", - "c", - "t", - "e", - "d", - " ", - "t", - "o", - " ", - "{", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - "}", - "\"", - ")", - "\n", - "e", - "l", - "s", - "e", - ":", - "\n", - " ", - " ", - " ", - " ", - "#", - " ", - "N", - "o", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "a", - "v", - "a", - "i", - "l", - "a", - "b", - "l", - "e", - " ", - "—", - " ", - "u", - "s", - "e", - " ", - "t", - "h", - "e", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "n", - "-", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - " ", - "e", - "n", - "g", - "i", - "n", - "e", - ".", - "\n", - " ", - " ", - " ", - " ", - "t", - "r", - "y", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "_", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - "\n", - " ", - " ", - " ", - " ", - "e", - "x", - "c", - "e", - "p", - "t", - " ", - "I", - "m", - "p", - "o", - "r", - "t", - "E", - "r", - "r", - "o", - "r", - " ", - "a", - "s", - " ", - "e", - "x", - "c", - ":", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "r", - "a", - "i", - "s", - "e", - " ", - "R", - "u", - "n", - "t", - "i", - "m", - "e", - "E", - "r", - "r", - "o", - "r", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - "e", - "d", - ".", - " ", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - "R", - "u", - "n", - ":", - " ", - "p", - "i", - "p", - " ", - "i", - "n", - "s", - "t", - "a", - "l", - "l", - " ", - "g", - "i", - "t", - "+", - "h", - "t", - "t", - "p", - "s", - ":", - "/", - "/", - "g", - "i", - "t", - "h", - "u", - "b", - ".", - "c", - "o", - "m", - "/", - "s", - "t", - "r", - "u", - "c", - "t", - "u", - "r", - "e", - "d", - "-", - "w", - "o", - "r", - "l", - "d", - "/", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "p", - "y", - "t", - "h", - "o", - "n", - ".", - "g", - "i", - "t", - "@", - "b", - "d", - "8", - "d", - "0", - "d", - "c", - "#", - "s", - "u", - "b", - "d", - "i", - "r", - "e", - "c", - "t", - "o", - "r", - "y", - "=", - "c", - "o", - "o", - "r", - "d", - "i", - "n", - "o", - "d", - "e", - "-", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "\"", - " ", - "—", - " ", - "o", - "r", - " ", - "s", - "t", - "a", - "r", - "t", - " ", - "a", - " ", - "C", - "o", - "o", - "r", - "d", - "i", - "N", - "o", - "d", - "e", - " ", - "s", - "e", - "r", - "v", - "e", - "r", - " ", - "a", - "n", - "d", - " ", - "s", - "e", - "t", - " ", - "C", - "O", - "O", - "R", - "D", - "I", - "N", - "O", - "D", - "E", - "_", - "A", - "D", - "D", - "R", - ".", - "\"", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - ")", - " ", - "f", - "r", - "o", - "m", - " ", - "e", - "x", - "c", - "\n", - "\n", - " ", - " ", - " ", - " ", - "_", - "l", - "c", - " ", - "=", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - "(", - "\"", - ":", - "m", - "e", - "m", - "o", - "r", - "y", - ":", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "c", - "l", - "i", - "e", - "n", - "t", - " ", - "=", - " ", - "_", - "E", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - "A", - "d", - "a", - "p", - "t", - "e", - "r", - "(", - "_", - "l", - "c", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "U", - "s", - "i", - "n", - "g", - " ", - "e", - "m", - "b", - "e", - "d", - "d", - "e", - "d", - " ", - "L", - "o", - "c", - "a", - "l", - "C", - "l", - "i", - "e", - "n", - "t", - " ", - "(", - "i", - "n", - "-", - "p", - "r", - "o", - "c", - "e", - "s", - "s", - ")", - "\"", - ")" + "import os, socket\n", + "\n", + "\n", + "def _port_open(port):\n", + " try:\n", + " with socket.create_connection((\"127.0.0.1\", port), timeout=1):\n", + " return True\n", + " except OSError:\n", + " return False\n", + "\n", + "\n", + "if os.environ.get(\"COORDINODE_ADDR\"):\n", + " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + "else:\n", + " try:\n", + " grpc_port = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " except ValueError as exc:\n", + " raise RuntimeError(\"COORDINODE_PORT must be an integer\") from exc\n", + "\n", + " if _port_open(grpc_port):\n", + " COORDINODE_ADDR = f\"localhost:{grpc_port}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " else:\n", + " # No server available — use the embedded in-process engine.\n", + " # Works without Docker or any external service; data is in-memory.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " _lc = LocalClient(\":memory:\")\n", + " client = _EmbeddedAdapter(_lc)\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { @@ -1495,570 +262,22 @@ "metadata": {}, "outputs": [], "source": [ - "i", - "f", - " ", - "n", - "o", - "t", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - ".", - "g", - "e", - "t", - "(", - "\"", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - "\"", - ")", - ":", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "'", - "S", - "k", - "i", - "p", - "p", - "i", - "n", - "g", - ":", - " ", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - " ", - "i", - "s", - " ", - "n", - "o", - "t", - " ", - "s", - "e", - "t", - ".", - " ", - "S", - "e", - "t", - " ", - "i", - "t", - " ", - "v", - "i", - "a", - " ", - "o", - "s", - ".", - "e", - "n", - "v", - "i", - "r", - "o", - "n", - "[", - "\"", - "O", - "P", - "E", - "N", - "A", - "I", - "_", - "A", - "P", - "I", - "_", - "K", - "E", - "Y", - "\"", - "]", - " ", - "=", - " ", - "\"", - "s", - "k", - "-", - ".", - ".", - ".", - "\"", - " ", - "a", - "n", - "d", - " ", - "r", - "e", - "-", - "r", - "u", - "n", - " ", - "t", - "h", - "i", - "s", - " ", - "c", - "e", - "l", - "l", - ".", - "'", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - "e", - "l", - "s", - "e", - ":", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - ".", - "c", - "h", - "a", - "i", - "n", - "s", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "G", - "r", - "a", - "p", - "h", - "C", - "y", - "p", - "h", - "e", - "r", - "Q", - "A", - "C", - "h", - "a", - "i", - "n", - "\n", - " ", - " ", - " ", - " ", - "f", - "r", - "o", - "m", - " ", - "l", - "a", - "n", - "g", - "c", - "h", - "a", - "i", - "n", - "_", - "o", - "p", - "e", - "n", - "a", - "i", - " ", - "i", - "m", - "p", - "o", - "r", - "t", - " ", - "C", - "h", - "a", - "t", - "O", - "p", - "e", - "n", - "A", - "I", - "\n", - "\n", - " ", - " ", - " ", - " ", - "c", - "h", - "a", - "i", - "n", - " ", - "=", - " ", - "G", - "r", - "a", - "p", - "h", - "C", - "y", - "p", - "h", - "e", - "r", - "Q", - "A", - "C", - "h", - "a", - "i", - "n", - ".", - "f", - "r", - "o", - "m", - "_", - "l", - "l", - "m", - "(", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "C", - "h", - "a", - "t", - "O", - "p", - "e", - "n", - "A", - "I", - "(", - "m", - "o", - "d", - "e", - "l", - "=", - "\"", - "g", - "p", - "t", - "-", - "4", - "o", - "-", - "m", - "i", - "n", - "i", - "\"", - ",", - " ", - "t", - "e", - "m", - "p", - "e", - "r", - "a", - "t", - "u", - "r", - "e", - "=", - "0", - ")", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "g", - "r", - "a", - "p", - "h", - "=", - "g", - "r", - "a", - "p", - "h", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "v", - "e", - "r", - "b", - "o", - "s", - "e", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - "a", - "l", - "l", - "o", - "w", - "_", - "d", - "a", - "n", - "g", - "e", - "r", - "o", - "u", - "s", - "_", - "r", - "e", - "q", - "u", - "e", - "s", - "t", - "s", - "=", - "T", - "r", - "u", - "e", - ",", - "\n", - " ", - " ", - " ", - " ", - ")", - "\n", - " ", - " ", - " ", - " ", - "r", - "e", - "s", - "u", - "l", - "t", - " ", - "=", - " ", - "c", - "h", - "a", - "i", - "n", - ".", - "i", - "n", - "v", - "o", - "k", - "e", - "(", - "f", - "\"", - "W", - "h", - "o", - " ", - "i", - "n", - "f", - "l", - "u", - "e", - "n", - "c", - "e", - "d", - " ", - "S", - "h", - "a", - "n", - "n", - "o", - "n", - "-", - "{", - "t", - "a", - "g", - "}", - "?", - "\"", - ")", - "\n", - " ", - " ", - " ", - " ", - "p", - "r", - "i", - "n", - "t", - "(", - "\"", - "A", - "n", - "s", - "w", - "e", - "r", - ":", - "\"", - ",", - " ", - "r", - "e", - "s", - "u", - "l", - "t", - "[", - "\"", - "r", - "e", - "s", - "u", - "l", - "t", - "\"", - "]", - ")" + "if not os.environ.get(\"OPENAI_API_KEY\"):\n", + " print(\n", + " 'Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.'\n", + " )\n", + "else:\n", + " from langchain.chains import GraphCypherQAChain\n", + " from langchain_openai import ChatOpenAI\n", + "\n", + " chain = GraphCypherQAChain.from_llm(\n", + " ChatOpenAI(model=\"gpt-4o-mini\", temperature=0),\n", + " graph=graph,\n", + " verbose=True,\n", + " allow_dangerous_requests=True,\n", + " )\n", + " result = chain.invoke(f\"Who influenced Shannon-{tag}?\")\n", + " print(\"Answer:\", result[\"result\"])" ] }, { @@ -2092,4 +311,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 64f7a22..02a3de2 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -121,7 +121,106 @@ "metadata": {}, "outputs": [], "source": [ - "import os, re, uuid\nfrom langchain_core.tools import tool\n\nSESSION = uuid.uuid4().hex # isolates this demo's data from other sessions\n\n_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n# Regex guards for query_facts (demo safety guard).\n_WRITE_CLAUSE_RE = re.compile(\n r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n re.IGNORECASE | re.DOTALL,\n)\n# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n# would pass yet return unscoped rows for `n`. A complete per-alias check would\n# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n# In production code, use server-side row-level security instead of client regex.\n_SESSION_WHERE_SCOPE_RE = re.compile(\n r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n re.IGNORECASE | re.DOTALL,\n)\n_SESSION_NODE_SCOPE_RE = re.compile(\n r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n re.IGNORECASE | re.DOTALL,\n)\n\n\n@tool\ndef save_fact(subject: str, relation: str, obj: str) -> str:\n \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n rel_type = relation.upper().replace(\" \", \"_\")\n # Validate rel_type before interpolating into Cypher to prevent injection.\n if not _REL_TYPE_RE.fullmatch(rel_type):\n return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n client.cypher(\n f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n f\"MERGE (a)-[r:{rel_type}]->(b)\",\n params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n )\n return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n\n\n@tool\ndef query_facts(cypher: str) -> str:\n \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n Must scope reads via either WHERE .session = $sess\n or a node pattern {session: $sess}.\"\"\"\n q = cypher.strip()\n if _WRITE_CLAUSE_RE.search(q):\n return \"Only read-only Cypher is allowed in query_facts.\"\n # Require $sess in a WHERE clause or node pattern, not just anywhere.\n # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n # Enforce a result cap so agents cannot dump the entire graph.\n # Cap or append LIMIT 20 before execution so the server never materializes\n # more than 20 rows, even when the LLM writes an explicit large LIMIT.\n _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+\\d+\\b\", re.IGNORECASE)\n if _LIMIT_RE.search(q):\n q = _LIMIT_RE.sub(\"LIMIT 20\", q)\n else:\n q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n rows = client.cypher(q, params={\"sess\": SESSION})\n return str(rows) if rows else \"No results\"\n\n\n@tool\ndef find_related(entity_name: str, depth: int = 1) -> str:\n \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n safe_depth = max(1, min(int(depth), 3))\n rows = client.cypher(\n f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n params={\"name\": entity_name, \"sess\": SESSION},\n )\n if not rows:\n return f\"No related entities found for {entity_name}\"\n return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n\n\n@tool\ndef list_all_facts() -> str:\n \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n rows = client.cypher(\n \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n params={\"sess\": SESSION},\n )\n if not rows:\n return \"No facts stored yet\"\n return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n\n\ntools = [save_fact, query_facts, find_related, list_all_facts]\nprint(f\"Session: {SESSION}\")\nprint(\"Tools:\", [t.name for t in tools])" + "import os, re, uuid\n", + "from langchain_core.tools import tool\n", + "\n", + "SESSION = uuid.uuid4().hex # isolates this demo's data from other sessions\n", + "\n", + "_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n", + "# Regex guards for query_facts (demo safety guard).\n", + "_WRITE_CLAUSE_RE = re.compile(\n", + " r\"\\b(CREATE|MERGE|DELETE|DETACH|SET|REMOVE|DROP|CALL|LOAD)\\b\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "# NOTE: this guard checks that AT LEAST ONE node pattern carries session scope.\n", + "# A Cartesian-product query such as `MATCH (n), (m {session: $sess}) RETURN n`\n", + "# would pass yet return unscoped rows for `n`. A complete per-alias check would\n", + "# require parsing the Cypher AST, which is out of scope for a demo safety guard.\n", + "# In production code, use server-side row-level security instead of client regex.\n", + "_SESSION_WHERE_SCOPE_RE = re.compile(\n", + " r\"WHERE\\b[^;{}]*\\.session\\s*=\\s*\\$sess\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "_SESSION_NODE_SCOPE_RE = re.compile(\n", + " r\"\\([^)]*\\{[^}]*session\\s*:\\s*\\$sess[^}]*\\}[^)]*\\)\",\n", + " re.IGNORECASE | re.DOTALL,\n", + ")\n", + "\n", + "\n", + "@tool\n", + "def save_fact(subject: str, relation: str, obj: str) -> str:\n", + " \"\"\"Save a fact (subject → relation → object) into the knowledge graph.\n", + " Example: save_fact('Alice', 'WORKS_AT', 'Acme Corp')\"\"\"\n", + " rel_type = relation.upper().replace(\" \", \"_\")\n", + " # Validate rel_type before interpolating into Cypher to prevent injection.\n", + " if not _REL_TYPE_RE.fullmatch(rel_type):\n", + " return f\"Invalid relation type {relation!r}: only letters, digits, and underscores allowed\"\n", + " client.cypher(\n", + " f\"MERGE (a:Entity {{name: $s, session: $sess}}) \"\n", + " f\"MERGE (b:Entity {{name: $o, session: $sess}}) \"\n", + " f\"MERGE (a)-[r:{rel_type}]->(b)\",\n", + " params={\"s\": subject, \"o\": obj, \"sess\": SESSION},\n", + " )\n", + " return f\"Saved: {subject} -[{rel_type}]-> {obj}\"\n", + "\n", + "\n", + "@tool\n", + "def query_facts(cypher: str) -> str:\n", + " \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n", + " Must scope reads via either WHERE .session = $sess\n", + " or a node pattern {session: $sess}.\"\"\"\n", + " q = cypher.strip()\n", + " if _WRITE_CLAUSE_RE.search(q):\n", + " return \"Only read-only Cypher is allowed in query_facts.\"\n", + " # Require $sess in a WHERE clause or node pattern, not just anywhere.\n", + " # Accepts both: WHERE n.session = $sess and MATCH (n {session: $sess})\n", + " if not (_SESSION_WHERE_SCOPE_RE.search(q) or _SESSION_NODE_SCOPE_RE.search(q)):\n", + " return \"Query must scope reads to the current session with either WHERE .session = $sess or {session: $sess}\"\n", + " # Enforce a result cap so agents cannot dump the entire graph.\n", + " # Cap explicit LIMIT to 20 (preserves smaller limits like LIMIT 1),\n", + " # or append LIMIT 20 when no LIMIT clause is present.\n", + " _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+(\\d+)\\b\", re.IGNORECASE)\n", + " def _cap_limit(m):\n", + " return f\"LIMIT {min(int(m.group(1)), 20)}\"\n", + " if _LIMIT_RE.search(q):\n", + " q = _LIMIT_RE.sub(_cap_limit, q)\n", + " else:\n", + " q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n", + " rows = client.cypher(q, params={\"sess\": SESSION})\n", + " return str(rows) if rows else \"No results\"\n", + "\n", + "\n", + "@tool\n", + "def find_related(entity_name: str, depth: int = 1) -> str:\n", + " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", + " safe_depth = max(1, min(int(depth), 3))\n", + " rows = client.cypher(\n", + " f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", + " \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n", + " \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n", + " params={\"name\": entity_name, \"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return f\"No related entities found for {entity_name}\"\n", + " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + "\n", + "\n", + "@tool\n", + "def list_all_facts() -> str:\n", + " \"\"\"List every fact stored in the current session's knowledge graph.\"\"\"\n", + " rows = client.cypher(\n", + " \"MATCH (a:Entity {session: $sess})-[r]->(b:Entity {session: $sess}) \"\n", + " \"RETURN a.name AS subject, type(r) AS relation, b.name AS object\",\n", + " params={\"sess\": SESSION},\n", + " )\n", + " if not rows:\n", + " return \"No facts stored yet\"\n", + " return \"\\n\".join(f\"{r['subject']} -[{r['relation']}]-> {r['object']}\" for r in rows)\n", + "\n", + "\n", + "tools = [save_fact, query_facts, find_related, list_all_facts]\n", + "print(f\"Session: {SESSION}\")\n", + "print(\"Tools:\", [t.name for t in tools])" ] }, { @@ -303,4 +402,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +} diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index a93acdd..0796631 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -617,9 +617,11 @@ def test_text_search_returns_results(client): seed_id = rows[0]["node_id"] # Text index must be created explicitly; nodes written before index creation # are indexed immediately at DDL time. - idx_info = client.create_text_index(idx_name, label, "body") - assert isinstance(idx_info, TextIndexInfo) + idx_created = False try: + idx_info = client.create_text_index(idx_name, label, "body") + idx_created = True + assert isinstance(idx_info, TextIndexInfo) results = client.text_search(label, "machine learning", limit=5) assert isinstance(results, list) assert results, "text_search returned no results after index creation" @@ -633,8 +635,11 @@ def test_text_search_returns_results(client): assert r.score > 0 assert isinstance(r.snippet, str) finally: - client.drop_text_index(idx_name) - client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + try: + if idx_created: + client.drop_text_index(idx_name) + finally: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @_fts @@ -674,15 +679,20 @@ def test_text_search_fuzzy(client): f"CREATE (n:{label} {{tag: $tag, body: 'coordinode graph database'}})", params={"tag": tag}, ) - client.create_text_index(idx_name, label, "body") + idx_created = False try: + client.create_text_index(idx_name, label, "body") + idx_created = True # "coordinode" with a one-character typo — Levenshtein-1 fuzzy must match. results = client.text_search(label, "coordinod", fuzzy=True, limit=5) assert isinstance(results, list) assert results, "fuzzy text_search returned no results after index creation" finally: - client.drop_text_index(idx_name) - client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + try: + if idx_created: + client.drop_text_index(idx_name) + finally: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) @_fts @@ -698,8 +708,10 @@ def test_hybrid_text_vector_search_returns_results(client): params={"tag": tag, "vec": vec}, ) seed_id = rows[0]["node_id"] - client.create_text_index(idx_name, label, "body") + idx_created = False try: + client.create_text_index(idx_name, label, "body") + idx_created = True results = client.hybrid_text_vector_search( label, "graph neural", @@ -718,5 +730,8 @@ def test_hybrid_text_vector_search_returns_results(client): assert isinstance(r.score, float) assert r.score > 0 finally: - client.drop_text_index(idx_name) - client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DETACH DELETE n", params={"tag": tag}) + try: + if idx_created: + client.drop_text_index(idx_name) + finally: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DETACH DELETE n", params={"tag": tag}) From 14bd4023e339aa6e74dbab36dd8a005044130093 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 10:19:12 +0300 Subject: [PATCH 69/86] fix(notebooks,sdk): defer GRPC_PORT parsing and document index API - defer int(GRPC_PORT) evaluation to else-branch in 00_seed_data and 03_langgraph notebooks so a malformed COORDINODE_PORT does not raise when COORDINODE_ADDR is already set - add pinning rationale comment to pip install cells in 01_llama and 02_langchain: coordinode-embedded is pinned to a git commit (Rust build); pure-Python packages are intentionally unpinned - clarify FTS test section comment: tests exercise explicit create/drop lifecycle; test_text_search_empty_for_unindexed_label must not create an index - add docstrings to create_text_index and drop_text_index documenting the Cypher identifier restriction and backtick-escaping alternative --- coordinode/coordinode/client.py | 17 ++++- demo/notebooks/00_seed_data.ipynb | 42 ++++++----- .../01_llama_index_property_graph.ipynb | 75 ++++++++++++++++++- demo/notebooks/02_langchain_graph_chain.ipynb | 72 +++++++++++++++++- demo/notebooks/03_langgraph_agent.ipynb | 48 ++++++------ tests/integration/test_sdk.py | 7 +- 6 files changed, 211 insertions(+), 50 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 0d6e5cb..c79e5ca 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -573,13 +573,19 @@ async def create_text_index( """Create a full-text (BM25) index on one or more node properties. Args: - name: Unique index name (e.g. ``"article_body"``). - label: Node label to index (e.g. ``"Article"``). + name: Unique index name (e.g. ``"article_body"``). Must be a + simple Cypher identifier: letters, digits, and underscores only, + starting with a letter or underscore. Names with dashes or + spaces are not supported by this method; use raw :meth:`cypher` + with backtick-escaped identifiers instead. + label: Node label to index (e.g. ``"Article"``). Same identifier + restrictions as *name* apply. properties: Property name or list of property names to index - (e.g. ``"body"`` or ``["title", "body"]``). + (e.g. ``"body"`` or ``["title", "body"]``). Same identifier + restrictions apply. language: Default stemming/tokenization language (e.g. ``"english"``, ``"russian"``). Empty string uses the server default - (``"english"``). + (``"english"``). Same identifier restrictions apply. Returns: :class:`TextIndexInfo` with index metadata and document count. @@ -614,6 +620,9 @@ async def drop_text_index(self, name: str) -> None: Args: name: Index name previously passed to :meth:`create_text_index`. + Must be a simple Cypher identifier (letters, digits, underscores). + Use raw :meth:`cypher` with backtick-escaped identifiers for names + that contain dashes or spaces. Example:: diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 113cf01..14b70e6 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -76,33 +76,37 @@ " return False\n", "\n", "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", - " # No server available — use the embedded in-process engine.\n", " try:\n", - " from coordinode_embedded import LocalClient\n", - " except ImportError as exc:\n", - " raise RuntimeError(\n", - " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", - " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", - " ) from exc\n", - "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" + " grpc_port = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " except ValueError as exc:\n", + " raise RuntimeError(\"COORDINODE_PORT must be an integer\") from exc\n", + "\n", + " if _port_open(grpc_port):\n", + " COORDINODE_ADDR = f\"localhost:{grpc_port}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " else:\n", + " # No server available — use the embedded in-process engine.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 27926ad..a02eee0 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -40,7 +40,80 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")" + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", + " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", + " # platform-specific rustup-init binaries), and pinning a hash here would break\n", + " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", + " # execution (not piped to shell) is the rustup team's recommended trust model.\n", + " # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n", + " # the `IN_COLAB` check above already ensures this block never runs outside\n", + " # Colab sessions, so there is no risk of unintentional execution in local\n", + " # or server environments.\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "# coordinode-embedded is pinned to a specific git commit because it requires a Rust\n", + "# build (maturin/pyo3) and the embedded engine must match the Python SDK version.\n", + "# The remaining packages (coordinode, llama-index, etc.) are installed without pins:\n", + "# they are pure Python, release frequently, and pip resolves a compatible version.\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"llama-index-graph-stores-coordinode\",\n", + " \"llama-index-core\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"SDK installed\")" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 8701968..155dc0e 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -37,7 +37,77 @@ "metadata": {}, "outputs": [], "source": [ - "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", + " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", + " # platform-specific rustup-init binaries), and pinning a hash here would break\n", + " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", + " # execution (not piped to shell) is the rustup team's recommended trust model.\n", + " # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n", + " # the `IN_COLAB` check above already ensures this block never runs outside\n", + " # Colab sessions, so there is no risk of unintentional execution in local\n", + " # or server environments.\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "# coordinode-embedded is pinned to a specific git commit because it requires a Rust\n", + "# build (maturin/pyo3) and the embedded engine must match the Python SDK version.\n", + "# The remaining packages (coordinode, LangChain, etc.) are installed without pins:\n", + "# they are pure Python, release frequently, and pip resolves a compatible version.\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "print(\"SDK installed\")" ] }, { diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 02a3de2..1b7349c 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -68,8 +68,6 @@ " return False\n", "\n", "\n", - "GRPC_PORT = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", - "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", " from coordinode import CoordinodeClient\n", @@ -79,28 +77,34 @@ " client.close()\n", " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", - "elif _port_open(GRPC_PORT):\n", - " COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n", - " from coordinode import CoordinodeClient\n", - "\n", - " client = CoordinodeClient(COORDINODE_ADDR)\n", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", - " # No server available — use the embedded in-process engine.\n", " try:\n", - " from coordinode_embedded import LocalClient\n", - " except ImportError as exc:\n", - " raise RuntimeError(\n", - " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", - " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", - " ) from exc\n", - "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" + " grpc_port = int(os.environ.get(\"COORDINODE_PORT\", \"7080\"))\n", + " except ValueError as exc:\n", + " raise RuntimeError(\"COORDINODE_PORT must be an integer\") from exc\n", + "\n", + " if _port_open(grpc_port):\n", + " COORDINODE_ADDR = f\"localhost:{grpc_port}\"\n", + " from coordinode import CoordinodeClient\n", + "\n", + " client = CoordinodeClient(COORDINODE_ADDR)\n", + " if not client.health():\n", + " client.close()\n", + " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " else:\n", + " # No server available — use the embedded in-process engine.\n", + " try:\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " client = LocalClient(\":memory:\")\n", + " print(\"Using embedded LocalClient (in-process)\")" ] }, { diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 0796631..68f69eb 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -573,9 +573,10 @@ def test_vector_search_returns_results(client): # They are wrapped with @_fts so the suite stays green against older servers # (UNIMPLEMENTED gRPC status → xfail); against >=0.3.8 servers they are real passes. # -# FTS indexing is NOT automatic. Each test that expects non-empty results must -# first create a text index with CREATE TEXT INDEX (or client.create_text_index()) -# and drop it in the finally block. Tests that deliberately cover the "no-index" +# These tests intentionally exercise the explicit text-index lifecycle APIs. +# Schema-free graphs may still be auto-indexed by the server, but the tests +# below create/drop an index explicitly so create_text_index()/drop_text_index() +# are covered deterministically. Tests that deliberately cover the "no-index" # case (test_text_search_empty_for_unindexed_label) must NOT create an index. def _fts(fn): """Wrap an FTS test to handle servers without TextService. From 4efc7b287211f4cfcf203ce92c83773d7d72ff61 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 12:39:32 +0300 Subject: [PATCH 70/86] build(deps): bump coordinode-rs submodule to v0.3.15 - perf(storage): batch Extra-targeting deltas in DocumentMerge - perf(codec): switch UidEncoder/Decoder to StreamVByte Coder1234 - perf(query): reuse adjacency key buffer in graph traversal hot path - fix(query): percentileCont/percentileDisc now accept query parameters No proto changes; stubs unchanged. Update server image tag in docker-compose.yml and demo/docker-compose.yml to 0.3.15; update demo/README.md version references. --- coordinode-rs | 2 +- demo/README.md | 4 ++-- demo/docker-compose.yml | 2 +- docker-compose.yml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index 4c80fbe..5777554 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 4c80fbef57eeee16efa471dc544704d592972b4a +Subproject commit 5777554712118b08e3ae043478c41980907a7a0b diff --git a/demo/README.md b/demo/README.md index 3ab198b..21fa208 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,8 +13,8 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. -> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.14 (embedded engine used in Colab). -> The Docker Compose stack below uses the CoordiNode **server** image v0.3.14. +> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.15 (embedded engine used in Colab). +> The Docker Compose stack below uses the CoordiNode **server** image v0.3.15. ## Run locally (Docker Compose) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 3d2c98c..f6786c0 100644 --- a/demo/docker-compose.yml +++ b/demo/docker-compose.yml @@ -1,7 +1,7 @@ services: coordinode: # Keep version in sync with root docker-compose.yml - image: ghcr.io/structured-world/coordinode:0.3.14 + image: ghcr.io/structured-world/coordinode:0.3.15 container_name: demo-coordinode ports: - "127.0.0.1:37080:7080" # gRPC (native API) — localhost-only diff --git a/docker-compose.yml b/docker-compose.yml index 72b85f6..7b8c401 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ version: "3.9" services: coordinode: - image: ghcr.io/structured-world/coordinode:0.3.14 + image: ghcr.io/structured-world/coordinode:0.3.15 container_name: coordinode ports: - "7080:7080" # gRPC From 787ca58ee6fbc1657e66a6b56729ec0da1604faf Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 12:39:57 +0300 Subject: [PATCH 71/86] build(demo): update notebook pin to 4efc7b2 (coordinode-rs v0.3.15) --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 4 ++-- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 14b70e6..0a320e1 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,7 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" }, { "cell_type": "markdown", @@ -101,7 +101,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index a02eee0..f8fb1b6 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -83,7 +83,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -190,7 +190,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 155dc0e..f24c301 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -184,7 +184,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 1b7349c..52ad158 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" }, { "cell_type": "markdown", @@ -99,7 +99,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@bd8d0dc#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", From cb53d456aac3b62e2d104e931734e008bd191943 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 13:04:57 +0300 Subject: [PATCH 72/86] fix(notebooks): install langchain-coordinode from git pin; add health checks to 01_llama - 02_langchain: replace PyPI langchain-coordinode with git+https install from the pinned repo commit so CoordinodeGraph(client=...) constructor is available in Colab (PyPI v0.5.0 lacks this parameter) - 01_llama: add client.health() guard after CoordinodeClient construction in both gRPC branches so the setup cell fails early when the server is unreachable rather than silently passing with a lazy client --- demo/notebooks/01_llama_index_property_graph.ipynb | 8 +++++++- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index f8fb1b6..e51482c 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -169,6 +169,9 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " if not client.health():\n", + " client.close()\n", + " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " try:\n", @@ -181,6 +184,9 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " if not client.health():\n", + " client.close()\n", + " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", " # No server available — use the embedded in-process engine.\n", @@ -196,7 +202,7 @@ "\n", " _lc = LocalClient(\":memory:\")\n", " client = _EmbeddedAdapter(_lc)\n", - " print(\"Using embedded LocalClient (in-process)\")" + " print(\"Using embedded LocalClient (in-process)\")\n" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index f24c301..11fa6d0 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -99,7 +99,7 @@ " \"-q\",\n", " \"coordinode\",\n", " \"langchain\",\n", - " \"langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " ],\n", From b60326a06e748ec1fda4f74914f53811e68449fb Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 13:14:42 +0300 Subject: [PATCH 73/86] fix(notebooks): add nest_asyncio to langchain/langgraph notebooks; fix find_related Cypher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 02_langchain and 03_langgraph were missing nest_asyncio install+apply, causing "Cannot run the event loop while another loop is running" in Jupyter (CoordinodeClient sync wrapper calls asyncio.run() internally) - 03_langgraph: remove path variable p= from find_related Cypher — CoordiNode does not support MATCH p=(...) path binding; rewrite to MATCH (...)-[*1..N]->(...) RETURN DISTINCT m.name --- demo/notebooks/02_langchain_graph_chain.ipynb | 4 + demo/notebooks/03_langgraph_agent.ipynb | 80 +++++++++++++++++-- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 11fa6d0..7126459 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -102,11 +102,15 @@ " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", + " \"nest_asyncio\",\n", " ],\n", " check=True,\n", " timeout=300,\n", ")\n", "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", "print(\"SDK installed\")" ] }, diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 52ad158..d07c49c 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -37,7 +37,78 @@ "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n # no need to spend 5+ minutes building coordinode-embedded from source.\n # The `IN_COLAB` check already guards against local/server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded in Colab only (requires Rust build).\n", + "if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", + " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", + " # platform-specific rustup-init binaries), and pinning a hash here would break\n", + " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", + " # execution (not piped to shell) is the rustup team's recommended trust model.\n", + " # Skip embedded build if COORDINODE_ADDR is set — user has a gRPC server,\n", + " # no need to spend 5+ minutes building coordinode-embedded from source.\n", + " # The `IN_COLAB` check already guards against local/server environments.\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"langchain-coordinode\",\n", + " \"langchain-community\",\n", + " \"langchain-openai\",\n", + " \"langgraph\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"SDK installed\")" + ] }, { "cell_type": "markdown", @@ -199,14 +270,13 @@ " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", " safe_depth = max(1, min(int(depth), 3))\n", " rows = client.cypher(\n", - " f\"MATCH p=(n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", - " \"WHERE ALL(x IN nodes(p) WHERE x.session = $sess) \"\n", - " \"RETURN m.name AS related, type(last(relationships(p))) AS via LIMIT 20\",\n", + " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", + " \"RETURN DISTINCT m.name AS related LIMIT 20\",\n", " params={\"name\": entity_name, \"sess\": SESSION},\n", " )\n", " if not rows:\n", " return f\"No related entities found for {entity_name}\"\n", - " return \"\\n\".join(f\"{r['via']} -> {r['related']}\" for r in rows)\n", + " return \"\\n\".join(r['related'] for r in rows)\n", "\n", "\n", "@tool\n", From 9deb7f0ee882966e8c51cabef4a6b621a6971ca7 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 13:15:55 +0300 Subject: [PATCH 74/86] docs(demo): clarify pinning in README and 02_langchain comment - demo/README.md: reword note to say the embedded Colab install is commit-pinned; Colab links target main (not pinned) - 02_langchain: update install cell comment to mention that both coordinode-embedded and langchain-coordinode are pinned (git commit); remaining pure-Python packages remain unpinned --- demo/README.md | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/demo/README.md b/demo/README.md index 21fa208..8d8eada 100644 --- a/demo/README.md +++ b/demo/README.md @@ -13,7 +13,7 @@ Interactive notebooks for LlamaIndex, LangChain, and LangGraph integrations. > **Note:** First run installs `coordinode-embedded` from source (Rust build, ~5 min). > Subsequent runs use Colab's pip cache. -> Notebooks are pinned to a specific commit that bundles coordinode-rs v0.3.15 (embedded engine used in Colab). +> The embedded Colab install is pinned to a specific commit that bundles coordinode-rs v0.3.15; the Colab notebook links above target `main`. > The Docker Compose stack below uses the CoordiNode **server** image v0.3.15. ## Run locally (Docker Compose) diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 7126459..39ed2f4 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -86,8 +86,11 @@ " timeout=600,\n", " )\n", "\n", - "# coordinode-embedded is pinned to a specific git commit because it requires a Rust\n", - "# build (maturin/pyo3) and the embedded engine must match the Python SDK version.\n", + "# coordinode-embedded and langchain-coordinode are pinned to a specific git commit:\n", + "# - coordinode-embedded requires a Rust build (maturin/pyo3); the embedded engine\n", + "# must match the Python SDK version.\n", + "# - langchain-coordinode is pinned to the same commit so CoordinodeGraph(client=...)\n", + "# is available; this parameter is not yet released to PyPI.\n", "# The remaining packages (coordinode, LangChain, etc.) are installed without pins:\n", "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "subprocess.run(\n", From 9cee719104800a83979cdc64a8b48ad4f56624a3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 19:13:56 +0300 Subject: [PATCH 75/86] docs(demo): note path variable limitation in find_related query CoordiNode does not yet support MATCH p=.../nodes(p) path variables. Document why session isolation holds without it: all nodes are MERGE'd with session scope so cross-session paths cannot form in practice. --- demo/notebooks/03_langgraph_agent.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index d07c49c..93a8019 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -269,6 +269,7 @@ "def find_related(entity_name: str, depth: int = 1) -> str:\n", " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", " safe_depth = max(1, min(int(depth), 3))\n", + " # Note: session constraint is on both endpoints (n, m). Constraining\n # intermediate nodes via path variables (MATCH p=..., WHERE ALL(x IN nodes(p)...))\n # is not yet supported by CoordiNode — planned for a future release.\n # In practice, session isolation holds because all nodes are MERGE'd with\n # their session scope, so cross-session paths cannot form.\n", " rows = client.cypher(\n", " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", " \"RETURN DISTINCT m.name AS related LIMIT 20\",\n", From 9044d9b9bc6a29eb51bd87405b5ffcfe1a90e166 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 19:47:05 +0300 Subject: [PATCH 76/86] fix(notebooks): correct rustup invocation flags and StateGraph heading - Remove -s and -- from subprocess.run(["/bin/sh", _rustup_path, ...]): -s reads script from stdin, which is wrong when passing a file path. Correct form is ["/bin/sh", path, "-y", "-q"]. Applies to notebooks 01, 02, and 03. - Rename "LangGraph StatefulGraph" heading to "LangGraph StateGraph" in notebook 03: langgraph exports StateGraph, not StatefulGraph. --- demo/notebooks/01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index e51482c..a9fe073 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -69,7 +69,7 @@ " _f.write(_r.read())\n", " _rustup_path = _f.name\n", " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-y\", \"-q\"], check=True, timeout=300)\n", " finally:\n", " os.unlink(_rustup_path)\n", " # Add cargo to PATH so maturin/pip can find it.\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 39ed2f4..2e826a5 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -66,7 +66,7 @@ " _f.write(_r.read())\n", " _rustup_path = _f.name\n", " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-y\", \"-q\"], check=True, timeout=300)\n", " finally:\n", " os.unlink(_rustup_path)\n", " # Add cargo to PATH so maturin/pip can find it.\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 93a8019..f552b62 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -66,7 +66,7 @@ " _f.write(_r.read())\n", " _rustup_path = _f.name\n", " try:\n", - " subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-y\", \"-q\"], check=True, timeout=300)\n", " finally:\n", " os.unlink(_rustup_path)\n", " # Add cargo to PATH so maturin/pip can find it.\n", @@ -347,7 +347,7 @@ "id": "d4e5f6a7-0003-0000-0000-000000000010", "metadata": {}, "source": [ - "## 3. LangGraph StatefulGraph — manual workflow\n", + "## 3. LangGraph StateGraph — manual workflow\n", "\n", "Shows how to wire CoordiNode tool calls into a LangGraph state machine without an LLM." ] From 564d7b163fea2c6bad93ef5d21315415c317d5b4 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 21:46:25 +0300 Subject: [PATCH 77/86] fix(client,notebooks): use fullmatch for DDL validator; cap outer LIMIT in query_facts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - client.py: _CYPHER_IDENT_RE.fullmatch() instead of .match() — pattern is already anchored but fullmatch is explicit for DDL safety - base.py, graph.py: add comments explaining SET r += {} is intentional (no-op supported since coordinode v0.3.12, removes conditional branch) - 03_langgraph_agent.ipynb: fix LIMIT bypass in query_facts sandbox guard; _LIMIT_RE matched inner clauses (WITH ... LIMIT 1), leaving outer result unbounded; replaced with _LIMIT_AT_END_RE anchored to end of query --- coordinode/coordinode/client.py | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 6 +++--- langchain-coordinode/langchain_coordinode/graph.py | 3 +++ .../llama_index/graph_stores/coordinode/base.py | 3 +++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index c79e5ca..3299269 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -36,7 +36,7 @@ def _validate_cypher_identifier(value: str, param_name: str) -> None: """Raise :exc:`ValueError` if *value* is not a valid Cypher identifier.""" - if not isinstance(value, str) or not _CYPHER_IDENT_RE.match(value): + if not isinstance(value, str) or not _CYPHER_IDENT_RE.fullmatch(value): raise ValueError( f"{param_name} must be a valid Cypher identifier (letters, digits, underscores, " f"starting with a letter or underscore); got {value!r}" diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index f552b62..55c93be 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -254,11 +254,11 @@ " # Enforce a result cap so agents cannot dump the entire graph.\n", " # Cap explicit LIMIT to 20 (preserves smaller limits like LIMIT 1),\n", " # or append LIMIT 20 when no LIMIT clause is present.\n", - " _LIMIT_RE = re.compile(r\"\\bLIMIT\\s+(\\d+)\\b\", re.IGNORECASE)\n", + " _LIMIT_AT_END_RE = re.compile(r\"\\bLIMIT\\s+(\\d+)\\s*;?\\s*$\", re.IGNORECASE | re.DOTALL)\n", " def _cap_limit(m):\n", " return f\"LIMIT {min(int(m.group(1)), 20)}\"\n", - " if _LIMIT_RE.search(q):\n", - " q = _LIMIT_RE.sub(_cap_limit, q)\n", + " if _LIMIT_AT_END_RE.search(q):\n", + " q = _LIMIT_AT_END_RE.sub(_cap_limit, q)\n", " else:\n", " q = q.rstrip().rstrip(\";\") + \" LIMIT 20\"\n", " rows = client.cypher(q, params={\"sess\": SESSION})\n", diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index b117cfe..62a85bf 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -215,6 +215,9 @@ def _create_edge(self, rel: Any) -> None: dst_label = _cypher_ident(rel.target.type or "Entity") rel_type = _cypher_ident(rel.type) props = dict(rel.properties or {}) + # SET r += $props is intentionally unconditional (even for empty dicts). + # CoordiNode ≥ v0.3.12 supports SET r += {} as a no-op, which lets us + # keep a single code path instead of branching on emptiness. self._client.cypher( f"MATCH (src:{src_label} {{name: $src}}) " f"MATCH (dst:{dst_label} {{name: $dst}}) " diff --git a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py index 960ca4d..88ae99b 100644 --- a/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py +++ b/llama-index-coordinode/llama_index/graph_stores/coordinode/base.py @@ -240,6 +240,9 @@ def upsert_relations(self, relations: list[Relation]) -> None: for rel in relations: props = rel.properties or {} label = _cypher_ident(rel.label) + # SET r += $props is intentionally unconditional (even for empty dicts). + # CoordiNode ≥ v0.3.12 supports SET r += {} as a no-op, which lets us + # keep a single code path instead of branching on emptiness. cypher = ( f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) " f"MERGE (src)-[r:{label}]->(dst) SET r += $props" From 94a9d6e5a6918b6e8410aba382f924819e9c1ada Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 22:10:34 +0300 Subject: [PATCH 78/86] chore: fix and extend .gitignore for version files and devlog --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index efc30ce..4241e28 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,13 @@ env/ # Version files generated by hatch-vcs coordinode/_version.py +coordinode/coordinode/_version.py langchain-coordinode/langchain_coordinode/_version.py llama-index-coordinode/llama_index/graph_stores/coordinode/_version.py + +# Local dev / arch docs — not part of the SDK GAPS.md CLAUDE.md +DEVLOG*.md + **/.ipynb_checkpoints/ From cf31c29654d53e28ed26f917197a90b6dc10c930 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 23:18:51 +0300 Subject: [PATCH 79/86] fix(client,graph,demo): preserve language in TextIndexInfo fallback; backfill schema text; add Dockerfile.jupyter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - client.py: fallback TextIndexInfo (empty CREATE TEXT INDEX rows) now includes default_language so callers always get the explicit language back - client.py: fix isinstance tuple → union type (ruff UP038) - graph.py: add _structured_to_text(); backfill self._schema when only get_labels()/get_edge_types() are available, keeping graph.schema consistent with graph.structured_schema - demo/Dockerfile.jupyter: add missing file referenced by docker-compose.yml - demo/notebooks: extract ACME_CORP constant (03); fix rustup flags (00) --- coordinode/coordinode/client.py | 6 +- demo/Dockerfile.jupyter | 32 +++++++++ demo/notebooks/00_seed_data.ipynb | 72 ++++++++++++++++++- demo/notebooks/03_langgraph_agent.ipynb | 12 ++-- .../langchain_coordinode/graph.py | 33 +++++++++ 5 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 demo/Dockerfile.jupyter diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 3299269..048853f 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -471,7 +471,7 @@ def _build_property_definitions( } if properties is None: return [] - if not isinstance(properties, (list, tuple)): + if not isinstance(properties, list | tuple): raise ValueError(f"'properties' must be a list of property dicts or None; got {type(properties).__name__}") result = [] for idx, p in enumerate(properties): @@ -613,7 +613,9 @@ async def create_text_index( rows = await self.cypher(cypher) if rows: return TextIndexInfo(rows[0]) - return TextIndexInfo({"index": name, "label": label, "properties": ", ".join(prop_list)}) + return TextIndexInfo( + {"index": name, "label": label, "properties": ", ".join(prop_list), "default_language": language} + ) async def drop_text_index(self, name: str) -> None: """Drop a full-text index by name. diff --git a/demo/Dockerfile.jupyter b/demo/Dockerfile.jupyter new file mode 100644 index 0000000..3ec7fba --- /dev/null +++ b/demo/Dockerfile.jupyter @@ -0,0 +1,32 @@ +FROM jupyter/scipy-notebook:latest + +USER root +RUN apt-get update && apt-get install -y --no-install-recommends gcc git && rm -rf /var/lib/apt/lists/* + +# Copy and chmod install script while still root +COPY install-sdk.sh /tmp/install-sdk.sh +RUN chmod +x /tmp/install-sdk.sh + +USER ${NB_UID} + +# Core graph + LLM orchestration stack +RUN pip install --no-cache-dir \ + # build tools for SDK editable installs (hatch-vcs reads git tags for versioning) + hatchling \ + hatch-vcs \ + nest_asyncio \ + # LlamaIndex core + graph store protocol + llama-index-core \ + llama-index-llms-openai \ + llama-index-embeddings-openai \ + # LangChain + LangGraph + langchain \ + langchain-openai \ + langchain-community \ + langgraph \ + # coordinode SDK packages (installed from mounted /sdk) + grpcio \ + grpcio-tools \ + protobuf + +WORKDIR /home/jovyan/work diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 0a320e1..8ba3254 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -50,7 +50,77 @@ "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": {}, "outputs": [], - "source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\nif IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n # never runs when a live gRPC server is available, so there is no risk of\n # unintentional execution in local or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"Ready\")" + "source": [ + "import os, sys, subprocess\n", + "\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "\n", + "# Install coordinode-embedded only when running in Colab AND no gRPC server is configured.\n", + "# If COORDINODE_ADDR is set, a live server is already available — skip the 5-min Rust build.\n", + "if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n", + " # Install Rust toolchain via rustup (https://rustup.rs).\n", + " # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n", + " # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly — this avoids\n", + " # piping remote content directly into a shell while maintaining HTTPS/TLS security\n", + " # through Python's default ssl context (cert-verified, TLS 1.2+).\n", + " # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n", + " # publish a stable per-release checksum for sh.rustup.rs itself (only for\n", + " # platform-specific rustup-init binaries), and pinning a hash here would break\n", + " # silently on every rustup release. The HTTPS/TLS verification + temp-file\n", + " # execution (not piped to shell) is the rustup team's recommended trust model.\n", + " # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n", + " # the `IN_COLAB and not COORDINODE_ADDR` check above already ensures this block\n", + " # never runs when a live gRPC server is available, so there is no risk of\n", + " # unintentional execution in local or server environments.\n", + " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", + "\n", + " _ctx = _ssl.create_default_context()\n", + " with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n", + " with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n", + " _f.write(_r.read())\n", + " _rustup_path = _f.name\n", + " try:\n", + " subprocess.run([\"/bin/sh\", _rustup_path, \"-y\", \"-q\"], check=True, timeout=300)\n", + " finally:\n", + " os.unlink(_rustup_path)\n", + " # Add cargo to PATH so maturin/pip can find it.\n", + " _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n", + " os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n", + " subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n", + " subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " ],\n", + " check=True,\n", + " timeout=600,\n", + " )\n", + "\n", + "subprocess.run(\n", + " [\n", + " sys.executable,\n", + " \"-m\",\n", + " \"pip\",\n", + " \"install\",\n", + " \"-q\",\n", + " \"coordinode\",\n", + " \"nest_asyncio\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + ")\n", + "\n", + "import nest_asyncio\n", + "\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"Ready\")" + ] }, { "cell_type": "markdown", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 55c93be..36a19f8 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -315,11 +315,13 @@ "metadata": {}, "outputs": [], "source": [ + "ACME_CORP = \"Acme Corp\" # constant used in several save_fact calls below\n", + "\n", "print(\"=== Saving facts ===\")\n", - "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"WORKS_AT\", \"obj\": \"Acme Corp\"}))\n", + "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"WORKS_AT\", \"obj\": ACME_CORP}))\n", "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"MANAGES\", \"obj\": \"Bob\"}))\n", - "print(save_fact.invoke({\"subject\": \"Bob\", \"relation\": \"WORKS_AT\", \"obj\": \"Acme Corp\"}))\n", - "print(save_fact.invoke({\"subject\": \"Acme Corp\", \"relation\": \"LOCATED_IN\", \"obj\": \"Berlin\"}))\n", + "print(save_fact.invoke({\"subject\": \"Bob\", \"relation\": \"WORKS_AT\", \"obj\": ACME_CORP}))\n", + "print(save_fact.invoke({\"subject\": ACME_CORP, \"relation\": \"LOCATED_IN\", \"obj\": \"Berlin\"}))\n", "print(save_fact.invoke({\"subject\": \"Alice\", \"relation\": \"KNOWS\", \"obj\": \"Charlie\"}))\n", "print(save_fact.invoke({\"subject\": \"Charlie\", \"relation\": \"EXPERT_IN\", \"obj\": \"Machine Learning\"}))\n", "\n", @@ -336,10 +338,10 @@ "print(\n", " query_facts.invoke(\n", " {\n", - " \"cypher\": 'MATCH (p:Entity {session: $sess})-[:WORKS_AT]->(c:Entity {name: \"Acme Corp\", session: $sess}) RETURN p.name AS employee'\n", + " \"cypher\": f'MATCH (p:Entity {{session: $sess}})-[:WORKS_AT]->(c:Entity {{name: \"{ACME_CORP}\", session: $sess}}) RETURN p.name AS employee'\n", " }\n", " )\n", - ")" + ")\n" ] }, { diff --git a/langchain-coordinode/langchain_coordinode/graph.py b/langchain-coordinode/langchain_coordinode/graph.py index 62a85bf..cc740f2 100644 --- a/langchain-coordinode/langchain_coordinode/graph.py +++ b/langchain-coordinode/langchain_coordinode/graph.py @@ -126,6 +126,11 @@ def refresh_schema(self) -> None: ] if node_props or rel_props: structured: dict[str, Any] = {"node_props": node_props, "rel_props": rel_props, "relationships": []} + # Backfill text schema for clients that expose get_labels()/get_edge_types() + # but not get_schema_text(). Keeps graph.schema consistent with + # graph.structured_schema so callers that read either view get the same data. + if not self._schema: + self._schema = _structured_to_text(node_props, rel_props) else: # Both APIs returned empty (e.g. schema-free graph or stub adapter) — # fall back to text parsing so we don't lose what get_schema_text() returned. @@ -392,6 +397,34 @@ def _cypher_ident(name: str) -> str: return f"`{name.replace('`', '``')}`" +def _structured_to_text( + node_props: dict[str, list[dict[str, str]]], + rel_props: dict[str, list[dict[str, str]]], +) -> str: + """Render node_props/rel_props dicts as a schema text string. + + Produces the same format that :func:`_parse_schema` consumes, so the two + functions are inverses. Used to backfill ``self._schema`` when the server + exposes ``get_labels()`` / ``get_edge_types()`` but not ``get_schema_text()``. + """ + lines: list[str] = ["Node labels:"] + for label, props in sorted(node_props.items()): + if props: + props_str = ", ".join(f"{p['property']}: {p['type']}" for p in props) + lines.append(f" - {label} (properties: {props_str})") + else: + lines.append(f" - {label}") + lines.append("") + lines.append("Edge types:") + for rel_type, props in sorted(rel_props.items()): + if props: + props_str = ", ".join(f"{p['property']}: {p['type']}" for p in props) + lines.append(f" - {rel_type} (properties: {props_str})") + else: + lines.append(f" - {rel_type}") + return "\n".join(lines) + + def _parse_schema(schema_text: str) -> dict[str, Any]: """Convert CoordiNode schema text into LangChain's structured format. From bc24c8d43ae16c70e247fa8386bc7a767d27c770 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Wed, 15 Apr 2026 23:19:14 +0300 Subject: [PATCH 80/86] build(notebooks): update pinned SDK commit to cf31c29 --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 6 +++--- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 8ba3254..b835281 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -95,7 +95,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -171,7 +171,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index a9fe073..8b52155 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -83,7 +83,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -196,7 +196,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 2e826a5..b746855 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ " \"-q\",\n", " \"coordinode\",\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", @@ -191,7 +191,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 36a19f8..4531ca8 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -170,7 +170,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@4efc7b287211f4cfcf203ce92c83773d7d72ff61#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", From 6e098a249b05b47c263233cd8cdb677546e46322 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 01:48:22 +0300 Subject: [PATCH 81/86] fix(schema): add schema_mode to create_edge_type, pin Dockerfile, health checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_edge_type(): add schema_mode parameter (strict/validated/flexible) matching create_label() — both async and sync variants - create_text_index(): use `language or "english"` in fallback TextIndexInfo so empty-string language resolves to the server default correctly - demo/Dockerfile.jupyter: pin base image from :latest to :python-3.11.6 - demo/install-sdk.sh: track untracked install script - notebooks 00, 02: add client.health() after CoordinodeClient() construction to surface connection errors early before running queries --- coordinode/coordinode/client.py | 33 +++++++++++++++++-- demo/Dockerfile.jupyter | 2 +- demo/install-sdk.sh | 7 ++++ demo/notebooks/00_seed_data.ipynb | 2 ++ demo/notebooks/02_langchain_graph_chain.ipynb | 2 ++ 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 demo/install-sdk.sh diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 048853f..2f9c109 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -471,6 +471,8 @@ def _build_property_definitions( } if properties is None: return [] + # list | tuple union syntax is valid in isinstance() for Python ≥3.10 (PEP 604). + # This project targets Python ≥3.11 (pyproject.toml: requires-python = ">=3.11"). if not isinstance(properties, list | tuple): raise ValueError(f"'properties' must be a list of property dicts or None; got {type(properties).__name__}") result = [] @@ -542,6 +544,8 @@ async def create_edge_type( self, name: str, properties: list[dict[str, Any]] | None = None, + *, + schema_mode: str = "strict", ) -> EdgeTypeInfo: """Create an edge type in the schema registry. @@ -550,15 +554,35 @@ async def create_edge_type( properties: Optional list of property dicts with keys ``name`` (str), ``type`` (str), ``required`` (bool), ``unique`` (bool). Same type strings as :meth:`create_label`. + schema_mode: ``"strict"`` (default — reject undeclared props), + ``"validated"`` (warn on extras), or ``"flexible"`` (allow any + prop without declaration). Case-insensitive; leading/trailing + whitespace is stripped. """ from coordinode._proto.coordinode.v1.graph.schema_pb2 import ( # type: ignore[import] CreateEdgeTypeRequest, PropertyDefinition, PropertyType, + SchemaMode, ) + _mode_map = { + "strict": SchemaMode.SCHEMA_MODE_STRICT, + "validated": SchemaMode.SCHEMA_MODE_VALIDATED, + "flexible": SchemaMode.SCHEMA_MODE_FLEXIBLE, + } + if not isinstance(schema_mode, str): + raise ValueError(f"schema_mode must be a str, got {type(schema_mode).__name__!r}") + schema_mode_normalized = schema_mode.strip().lower() + if schema_mode_normalized not in _mode_map: + raise ValueError(f"schema_mode must be one of {list(_mode_map)}, got {schema_mode!r}") + proto_props = self._build_property_definitions(properties, PropertyType, PropertyDefinition) - req = CreateEdgeTypeRequest(name=name, properties=proto_props) + req = CreateEdgeTypeRequest( + name=name, + properties=proto_props, + schema_mode=_mode_map[schema_mode_normalized], + ) et = await self._schema_stub.CreateEdgeType(req, timeout=self._timeout) return EdgeTypeInfo(et) @@ -613,8 +637,9 @@ async def create_text_index( rows = await self.cypher(cypher) if rows: return TextIndexInfo(rows[0]) + effective_language = language or "english" return TextIndexInfo( - {"index": name, "label": label, "properties": ", ".join(prop_list), "default_language": language} + {"index": name, "label": label, "properties": ", ".join(prop_list), "default_language": effective_language} ) async def drop_text_index(self, name: str) -> None: @@ -918,9 +943,11 @@ def create_edge_type( self, name: str, properties: list[dict[str, Any]] | None = None, + *, + schema_mode: str = "strict", ) -> EdgeTypeInfo: """Create an edge type in the schema registry.""" - return self._run(self._async.create_edge_type(name, properties)) + return self._run(self._async.create_edge_type(name, properties, schema_mode=schema_mode)) def create_text_index( self, diff --git a/demo/Dockerfile.jupyter b/demo/Dockerfile.jupyter index 3ec7fba..effccd9 100644 --- a/demo/Dockerfile.jupyter +++ b/demo/Dockerfile.jupyter @@ -1,4 +1,4 @@ -FROM jupyter/scipy-notebook:latest +FROM jupyter/scipy-notebook:python-3.11.6 USER root RUN apt-get update && apt-get install -y --no-install-recommends gcc git && rm -rf /var/lib/apt/lists/* diff --git a/demo/install-sdk.sh b/demo/install-sdk.sh new file mode 100644 index 0000000..4a860c9 --- /dev/null +++ b/demo/install-sdk.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Install coordinode SDK packages from mounted /sdk source. +# Run once inside the container after /sdk is mounted. +set -e +pip install --no-cache-dir -e /sdk/coordinode +pip install --no-cache-dir -e /sdk/llama-index-coordinode +pip install --no-cache-dir -e /sdk/langchain-coordinode diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index b835281..02eaa5a 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -151,6 +151,7 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " try:\n", @@ -163,6 +164,7 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", " # No server available — use the embedded in-process engine.\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index b746855..2365ef4 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -170,6 +170,7 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " try:\n", @@ -182,6 +183,7 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", + " client.health()\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", " # No server available — use the embedded in-process engine.\n", From 3c050e2be61de808fcdafe03d67f374a1d5f3dd6 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 01:51:26 +0300 Subject: [PATCH 82/86] build(notebooks): update pinned SDK commit to 6e098a2 --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 6 +++--- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 02eaa5a..6d13375 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -95,7 +95,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -173,7 +173,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 8b52155..401c842 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -83,7 +83,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -196,7 +196,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 2365ef4..8347af2 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ " \"-q\",\n", " \"coordinode\",\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", @@ -193,7 +193,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 4531ca8..41051ba 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -170,7 +170,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@cf31c29654d53e28ed26f917197a90b6dc10c930#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", From 1b30c682c47da71c4dc7f9f316cd150666163b3d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 01:56:09 +0300 Subject: [PATCH 83/86] style: fix UP038 isinstance union syntax in _types.py (Python >=3.11) --- coordinode/coordinode/_types.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coordinode/coordinode/_types.py b/coordinode/coordinode/_types.py index ef45edd..d8e354c 100644 --- a/coordinode/coordinode/_types.py +++ b/coordinode/coordinode/_types.py @@ -33,11 +33,13 @@ def to_property_value(py_val: PyValue) -> Any: pv.string_value = py_val elif isinstance(py_val, bytes): pv.bytes_value = py_val - elif isinstance(py_val, (list, tuple)): + elif isinstance(py_val, list | tuple): # Homogeneous float list → Vector; mixed/str list → PropertyList. # bool is a subclass of int, so exclude it explicitly — [True, False] must # not be serialised as a Vector of 1.0/0.0 but as a PropertyList. - if py_val and all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in py_val): + # list | tuple union syntax is valid in isinstance() for Python ≥3.10 (PEP 604). + # This project targets Python ≥3.11 (pyproject.toml: requires-python = ">=3.11"). + if py_val and all(isinstance(v, int | float) and not isinstance(v, bool) for v in py_val): vec = Vector(values=[float(v) for v in py_val]) pv.vector_value.CopyFrom(vec) else: From 518a61f43580373faa4c8d9e9d8c2197183cdaf9 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 01:58:44 +0300 Subject: [PATCH 84/86] style(notebook): split multi-line comment string into per-line source array entries --- demo/notebooks/03_langgraph_agent.ipynb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 41051ba..aa1c884 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -269,7 +269,11 @@ "def find_related(entity_name: str, depth: int = 1) -> str:\n", " \"\"\"Find all entities reachable from entity_name within the given number of hops (max 3).\"\"\"\n", " safe_depth = max(1, min(int(depth), 3))\n", - " # Note: session constraint is on both endpoints (n, m). Constraining\n # intermediate nodes via path variables (MATCH p=..., WHERE ALL(x IN nodes(p)...))\n # is not yet supported by CoordiNode — planned for a future release.\n # In practice, session isolation holds because all nodes are MERGE'd with\n # their session scope, so cross-session paths cannot form.\n", + " # Note: session constraint is on both endpoints (n, m). Constraining\n", + " # intermediate nodes via path variables (MATCH p=..., WHERE ALL(x IN nodes(p)...))\n", + " # is not yet supported by CoordiNode — planned for a future release.\n", + " # In practice, session isolation holds because all nodes are MERGE'd with\n", + " # their session scope, so cross-session paths cannot form.\n", " rows = client.cypher(\n", " f\"MATCH (n:Entity {{name: $name, session: $sess}})-[*1..{safe_depth}]->(m:Entity {{session: $sess}}) \"\n", " \"RETURN DISTINCT m.name AS related LIMIT 20\",\n", From 8da94d694ecaabee6f8380147d02f08220061bfa Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 02:00:34 +0300 Subject: [PATCH 85/86] fix(client,notebooks): validate properties type in create_text_index; document hybrid index prerequisite; guard health() return value in notebooks --- coordinode/coordinode/client.py | 10 +++++++++- demo/notebooks/00_seed_data.ipynb | 6 ++++-- demo/notebooks/02_langchain_graph_chain.ipynb | 6 ++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 2f9c109..eb85f50 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -623,8 +623,10 @@ async def create_text_index( _validate_cypher_identifier(label, "label") if isinstance(properties, str): prop_list = [properties] - else: + elif isinstance(properties, list | tuple): prop_list = list(properties) + else: + raise ValueError("'properties' must be a property name (str) or a list of property names") if not prop_list: raise ValueError("'properties' must contain at least one property name") for prop in prop_list: @@ -785,6 +787,12 @@ async def hybrid_text_vector_search( Returns: List of :class:`HybridResult` ordered by RRF score descending. + + Note: + A full-text index covering *label* **must exist** before calling this + method — create one with :meth:`create_text_index` or a + ``CREATE TEXT INDEX`` Cypher statement. Calling this method on a + label without a text index returns an empty list. """ from coordinode._proto.coordinode.v1.query.text_pb2 import ( # type: ignore[import] HybridTextVectorSearchRequest, diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 6d13375..507f623 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -151,7 +151,8 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " try:\n", @@ -164,7 +165,8 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", " # No server available — use the embedded in-process engine.\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 8347af2..7b0480d 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -170,7 +170,8 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", " try:\n", @@ -183,7 +184,8 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " client.health()\n", + " if not client.health():\n", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", " # No server available — use the embedded in-process engine.\n", From 13177557571d61d26db52c6867323ed28397d545 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 16 Apr 2026 02:01:54 +0300 Subject: [PATCH 86/86] build(notebooks): update pinned SDK commit to 8da94d6 --- demo/notebooks/00_seed_data.ipynb | 4 ++-- demo/notebooks/01_llama_index_property_graph.ipynb | 4 ++-- demo/notebooks/02_langchain_graph_chain.ipynb | 6 +++--- demo/notebooks/03_langgraph_agent.ipynb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 507f623..7a1eaf8 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -95,7 +95,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -175,7 +175,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 401c842..805b40c 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -83,7 +83,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -196,7 +196,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 7b0480d..0ce43fd 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ " \"-q\",\n", " \"coordinode\",\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", @@ -195,7 +195,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index aa1c884..1379cc7 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -80,7 +80,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -170,7 +170,7 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@6e098a249b05b47c263233cd8cdb677546e46322#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n",