From 4b0274bcbc2b80339bc89bc5f7239a5853393bbc Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 13:10:36 +0300 Subject: [PATCH 01/25] feat(sdk): update for coordinode-server v0.4.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: HybridTextVectorSearch RPC removed upstream. - Bump coordinode-rs submodule to v0.4.1 - Bump proto submodule to eb472a4 (HybridTextVectorSearch removed) - Remove CoordinodeClient.hybrid_text_vector_search (async + sync) — replaced by Cypher hybrid scoring (rrf_score / hybrid_score / text_score) - Drop matching integration test - Bump docker-compose image tag to 0.4.1 (root + demo) - Re-execute all 4 demo notebooks against v0.4.1 Closes #46 --- coordinode-rs | 2 +- coordinode/coordinode/client.py | 82 ----- demo/docker-compose.yml | 2 +- demo/notebooks/00_seed_data.ipynb | 207 +++++++++-- .../01_llama_index_property_graph.ipynb | 322 ++++++++++++++++-- demo/notebooks/02_langchain_graph_chain.ipynb | 288 ++++++++++++++-- demo/notebooks/03_langgraph_agent.ipynb | 206 +++++++++-- docker-compose.yml | 2 +- proto | 2 +- tests/integration/test_sdk.py | 43 --- 10 files changed, 914 insertions(+), 242 deletions(-) diff --git a/coordinode-rs b/coordinode-rs index 21ae71c..e0694e5 160000 --- a/coordinode-rs +++ b/coordinode-rs @@ -1 +1 @@ -Subproject commit 21ae71c381a0f1cb2bd625410fc5585a9d6d080c +Subproject commit e0694e583a4ccb7e42fd29bfff89b51ce9964e72 diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 1587556..bcbc488 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -776,64 +776,6 @@ async def text_search( 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). 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 - ``"embedding"``). - - 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. - """ - if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1: - raise ValueError(f"limit must be an integer >= 1, got {limit!r}.") - from coordinode._proto.coordinode.v1.query.text_pb2 import ( # type: ignore[import] - HybridTextVectorSearchRequest, - ) - - req = HybridTextVectorSearchRequest( - label=label, - text_query=text_query, - vector=[float(v) for v in 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, @@ -1016,30 +958,6 @@ def text_search( """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()) diff --git a/demo/docker-compose.yml b/demo/docker-compose.yml index 48ba3f8..39b924a 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.17 + image: ghcr.io/structured-world/coordinode:0.4.1 container_name: demo-coordinode ports: - "127.0.0.1:37080:7080" # gRPC (native API) — localhost-only diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 6961f92..707a285 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -46,10 +46,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "a1b2c3d4-0000-0000-0000-000000000003", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:06.039243Z", + "iopub.status.busy": "2026-04-19T10:04:06.039172Z", + "iopub.status.idle": "2026-04-19T10:04:06.970761Z", + "shell.execute_reply": "2026-04-19T10:04:06.970069Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ready\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" + ] + } + ], "source": [ "import os, sys, subprocess\n", "\n", @@ -129,14 +153,35 @@ "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 `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." + "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", - "execution_count": null, + "execution_count": 2, "id": "a1b2c3d4-0000-0000-0000-000000000005", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:06.972780Z", + "iopub.status.busy": "2026-04-19T10:04:06.972654Z", + "iopub.status.idle": "2026-04-19T10:04:07.012113Z", + "shell.execute_reply": "2026-04-19T10:04:07.011321Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to 127.0.0.1:37080\n" + ] + } + ], "source": [ "import os, socket\n", "\n", @@ -196,10 +241,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "a1b2c3d4-0000-0000-0000-000000000007", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:07.014105Z", + "iopub.status.busy": "2026-04-19T10:04:07.013909Z", + "iopub.status.idle": "2026-04-19T10:04:07.020088Z", + "shell.execute_reply": "2026-04-19T10:04:07.019603Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using DEMO_TAG: seed_data_7cfe797a\n", + "Previous demo data removed\n" + ] + } + ], "source": [ "import uuid\n", "\n", @@ -225,10 +286,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "a1b2c3d4-0000-0000-0000-000000000009", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:07.021785Z", + "iopub.status.busy": "2026-04-19T10:04:07.021661Z", + "iopub.status.idle": "2026-04-19T10:04:07.057106Z", + "shell.execute_reply": "2026-04-19T10:04:07.056653Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created 10 people\n", + "Created 6 companies\n", + "Created 8 technologies\n" + ] + } + ], "source": [ "# ── People ────────────────────────────────────────────────────────────────\n", "people = [\n", @@ -302,10 +380,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "a1b2c3d4-0000-0000-0000-000000000011", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:07.058969Z", + "iopub.status.busy": "2026-04-19T10:04:07.058874Z", + "iopub.status.idle": "2026-04-19T10:04:07.100906Z", + "shell.execute_reply": "2026-04-19T10:04:07.100443Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created 34 relationships\n" + ] + } + ], "source": [ "edges = [\n", " # WORKS_AT\n", @@ -391,10 +484,38 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "a1b2c3d4-0000-0000-0000-000000000013", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:07.102456Z", + "iopub.status.busy": "2026-04-19T10:04:07.102333Z", + "iopub.status.idle": "2026-04-19T10:04:07.110337Z", + "shell.execute_reply": "2026-04-19T10:04:07.109926Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Node counts:\n", + " Person 10\n", + " Company 6\n", + " Technology 8\n", + "\n", + "Relationship counts:\n", + " WORKS_AT 10\n", + " KNOWS 6\n", + " RESEARCHES 6\n", + " USES 5\n", + " BUILDS_ON 4\n", + " FOUNDED 1\n", + " CO_FOUNDED 1\n", + " ACQUIRED 1\n" + ] + } + ], "source": [ "from collections import Counter\n", "\n", @@ -419,10 +540,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "a1b2c3d4-0000-0000-0000-000000000014", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:07.112104Z", + "iopub.status.busy": "2026-04-19T10:04:07.111970Z", + "iopub.status.idle": "2026-04-19T10:04:07.118744Z", + "shell.execute_reply": "2026-04-19T10:04:07.118356Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Who works at Synthex? ===\n", + " Carol Smith — Founder & CEO\n", + " Eva Müller — Systems Architect\n", + " Henry Rossi — CTO\n", + "\n", + "=== What does Synthex use? ===\n", + " Knowledge Graph\n", + " Vector Database\n", + " RAG\n", + "\n", + "=== GraphRAG dependency chain ===\n", + " → Knowledge Graph\n", + " → RAG\n", + " → Vector Database\n", + "\n", + "✓ Demo data seeded.\n", + "To query it from notebooks 01–03, connect them to the same CoordiNode server (COORDINODE_ADDR).\n" + ] + } + ], "source": [ "print(\"=== Who works at Synthex? ===\")\n", "rows = client.cypher(\n", @@ -462,7 +614,16 @@ "name": "python3" }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 4cb0b53..e24649a 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -35,10 +35,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b2c3d4e5-0001-0000-0000-000000000003", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:13.854777Z", + "iopub.status.busy": "2026-04-19T10:04:13.854705Z", + "iopub.status.idle": "2026-04-19T10:04:15.115115Z", + "shell.execute_reply": "2026-04-19T10:04:15.114595Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SDK installed\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" + ] + } + ], "source": [ "import os, sys, subprocess\n", "\n", @@ -133,12 +157,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "b2c3d4e5-0001-0000-0000-000000000005", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.116903Z", + "iopub.status.busy": "2026-04-19T10:04:15.116806Z", + "iopub.status.idle": "2026-04-19T10:04:15.120140Z", + "shell.execute_reply": "2026-04-19T10:04:15.119700Z" + } + }, "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" + "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" ] }, { @@ -151,10 +205,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "b2c3d4e5-0001-0000-0000-000000000007", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.121236Z", + "iopub.status.busy": "2026-04-19T10:04:15.121171Z", + "iopub.status.idle": "2026-04-19T10:04:15.161579Z", + "shell.execute_reply": "2026-04-19T10:04:15.161134Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to 127.0.0.1:37080\n" + ] + } + ], "source": [ "import os, socket\n", "\n", @@ -220,10 +289,39 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "b2c3d4e5-0001-0000-0000-000000000009", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.163153Z", + "iopub.status.busy": "2026-04-19T10:04:15.162973Z", + "iopub.status.idle": "2026-04-19T10:04:15.761333Z", + "shell.execute_reply": "2026-04-19T10:04:15.760858Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Schema:\n", + "Node labels:\n", + " - Company\n", + " - Person\n", + " - Technology\n", + "\n", + "Edge types:\n", + " - ACQUIRED\n", + " - BUILDS_ON\n", + " - CO_FOUNDED\n", + " - FOUNDED\n", + " - KNOWS\n", + " - RESEARCHES\n", + " - USES\n", + " - WORKS_AT\n" + ] + } + ], "source": [ "from llama_index.graph_stores.coordinode import CoordinodePropertyGraphStore\n", "from llama_index.core.graph_stores.types import EntityNode, Relation\n", @@ -245,12 +343,47 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "b2c3d4e5-0001-0000-0000-000000000011", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.762887Z", + "iopub.status.busy": "2026-04-19T10:04:15.762722Z", + "iopub.status.idle": "2026-04-19T10:04:15.774422Z", + "shell.execute_reply": "2026-04-19T10:04:15.773770Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Upserted nodes: ['Alice-590af6188388463ba7e3f958c5d81a36', 'Bob-590af6188388463ba7e3f958c5d81a36', 'GraphRAG-590af6188388463ba7e3f958c5d81a36']\n", + "Upserted relations: ['RESEARCHES', 'COLLABORATES', 'IMPLEMENTS']\n" + ] + } + ], "source": [ - "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])" + "import uuid\n", + "\n", + "tag = uuid.uuid4().hex\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])" ] }, { @@ -263,10 +396,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "b2c3d4e5-0001-0000-0000-000000000013", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.775786Z", + "iopub.status.busy": "2026-04-19T10:04:15.775698Z", + "iopub.status.idle": "2026-04-19T10:04:15.782234Z", + "shell.execute_reply": "2026-04-19T10:04:15.781790Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Triplets for Alice-590af6188388463ba7e3f958c5d81a36:\n", + " Alice-590af6188388463ba7e3f958c5d81a36 --[COLLABORATES]--> Bob-590af6188388463ba7e3f958c5d81a36\n", + " Alice-590af6188388463ba7e3f958c5d81a36 --[RESEARCHES]--> GraphRAG-590af6188388463ba7e3f958c5d81a36\n" + ] + } + ], "source": [ "triplets = store.get_triplets(entity_names=[f\"Alice-{tag}\"])\n", "print(f\"Triplets for Alice-{tag}:\")\n", @@ -284,10 +434,27 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "b2c3d4e5-0001-0000-0000-000000000015", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.784083Z", + "iopub.status.busy": "2026-04-19T10:04:15.783972Z", + "iopub.status.idle": "2026-04-19T10:04:15.792506Z", + "shell.execute_reply": "2026-04-19T10:04:15.792155Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Rel map for Alice-590af6188388463ba7e3f958c5d81a36 (2 rows):\n", + " Alice-590af6188388463ba7e3f958c5d81a36 --[COLLABORATES]--> Bob-590af6188388463ba7e3f958c5d81a36\n", + " Alice-590af6188388463ba7e3f958c5d81a36 --[RESEARCHES]--> GraphRAG-590af6188388463ba7e3f958c5d81a36\n" + ] + } + ], "source": [ "found_alice = store.get(properties={\"name\": f\"Alice-{tag}\"})\n", "rel_map = store.get_rel_map(found_alice, depth=1, limit=20)\n", @@ -306,10 +473,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "b2c3d4e5-0001-0000-0000-000000000017", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.793712Z", + "iopub.status.busy": "2026-04-19T10:04:15.793643Z", + "iopub.status.idle": "2026-04-19T10:04:15.798524Z", + "shell.execute_reply": "2026-04-19T10:04:15.797951Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query result: [{'person': 'Alice-590af6188388463ba7e3f958c5d81a36', 'since': None, 'topic': 'GraphRAG-590af6188388463ba7e3f958c5d81a36'}]\n" + ] + } + ], "source": [ "rows = store.structured_query(\n", " \"MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)\"\n", @@ -330,10 +512,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "b2c3d4e5-0001-0000-0000-000000000019", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.800392Z", + "iopub.status.busy": "2026-04-19T10:04:15.800302Z", + "iopub.status.idle": "2026-04-19T10:04:15.805441Z", + "shell.execute_reply": "2026-04-19T10:04:15.805001Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Node labels:\n", + " - Company\n", + " - Person\n", + " - Technology\n", + " - Topic\n", + "\n", + "Edge types:\n", + " - ACQUIRED\n", + " - BUILDS_ON\n", + " - COLLABORATES\n", + " - CO_FOUNDED\n", + " - FOUNDED\n", + " - IMPLEMENTS\n", + " - KNOWS\n", + " - RESEARCHES\n", + " - USES\n", + " - WORKS_AT\n" + ] + } + ], "source": [ "schema = store.get_schema()\n", "print(schema)" @@ -349,10 +562,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "b2c3d4e5-0001-0000-0000-000000000021", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.806831Z", + "iopub.status.busy": "2026-04-19T10:04:15.806753Z", + "iopub.status.idle": "2026-04-19T10:04:15.823143Z", + "shell.execute_reply": "2026-04-19T10:04:15.822393Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Edge count after double upsert (expect 1): 1\n" + ] + } + ], "source": [ "store.upsert_relations(relations) # second call — should still be exactly 1 edge\n", "rows = store.structured_query(\n", @@ -372,10 +600,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "id": "b2c3d4e5-0001-0000-0000-000000000023", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:15.824642Z", + "iopub.status.busy": "2026-04-19T10:04:15.824537Z", + "iopub.status.idle": "2026-04-19T10:04:15.840013Z", + "shell.execute_reply": "2026-04-19T10:04:15.839478Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cleaned up\n" + ] + } + ], "source": [ "store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n", "print(\"Cleaned up\")\n", @@ -391,7 +634,16 @@ "name": "python3" }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index cd005ef..c4d1c14 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -32,10 +32,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "c3d4e5f6-0002-0000-0000-000000000003", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:18.638106Z", + "iopub.status.busy": "2026-04-19T10:04:18.638034Z", + "iopub.status.idle": "2026-04-19T10:04:37.524798Z", + "shell.execute_reply": "2026-04-19T10:04:37.524144Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SDK installed\n" + ] + } + ], "source": [ "import os, sys, subprocess\n", "\n", @@ -134,12 +158,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "c3d4e5f6-0002-0000-0000-000000000005", - "metadata": {}, + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.526604Z", + "iopub.status.busy": "2026-04-19T10:04:37.526462Z", + "iopub.status.idle": "2026-04-19T10:04:37.529848Z", + "shell.execute_reply": "2026-04-19T10:04:37.529456Z" + } + }, "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" + "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" ] }, { @@ -152,10 +206,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "c3d4e5f6-0002-0000-0000-000000000007", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.531142Z", + "iopub.status.busy": "2026-04-19T10:04:37.531071Z", + "iopub.status.idle": "2026-04-19T10:04:37.574066Z", + "shell.execute_reply": "2026-04-19T10:04:37.573499Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to 127.0.0.1:37080\n" + ] + } + ], "source": [ "import os, socket\n", "\n", @@ -219,10 +288,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "c3d4e5f6-0002-0000-0000-000000000009", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.575712Z", + "iopub.status.busy": "2026-04-19T10:04:37.575560Z", + "iopub.status.idle": "2026-04-19T10:04:37.645810Z", + "shell.execute_reply": "2026-04-19T10:04:37.645347Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Schema preview:\n", + "Node labels:\n", + " - Company\n", + " - Person\n", + " - Technology\n", + "\n", + "Edge types:\n", + " - ACQUIRED\n", + " - BUILDS_ON\n", + " - COLLABORATES\n", + " - CO_FOUNDED\n", + " - FOUNDED\n", + " - IMPLEMENTS\n", + " - KNOWS\n", + " - RESEARCHES\n", + " - USES\n", + " - WORKS_AT\n" + ] + } + ], "source": [ "import os, uuid\n", "from langchain_coordinode import CoordinodeGraph\n", @@ -247,12 +347,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "c3d4e5f6-0002-0000-0000-000000000011", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.647188Z", + "iopub.status.busy": "2026-04-19T10:04:37.647116Z", + "iopub.status.idle": "2026-04-19T10:04:37.658568Z", + "shell.execute_reply": "2026-04-19T10:04:37.658042Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Documents added\n" + ] + } + ], "source": [ - "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\")" + "tag = uuid.uuid4().hex\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\")" ] }, { @@ -265,10 +394,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "c3d4e5f6-0002-0000-0000-000000000013", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.660044Z", + "iopub.status.busy": "2026-04-19T10:04:37.659935Z", + "iopub.status.idle": "2026-04-19T10:04:37.663799Z", + "shell.execute_reply": "2026-04-19T10:04:37.663378Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Scientists → Fields:\n", + " Turing-a0a7f80c9ed8491d818da73110a9b2fa --[FOUNDED]--> Cryptography-a0a7f80c9ed8491d818da73110a9b2fa\n" + ] + } + ], "source": [ "rows = graph.query(\n", " \"MATCH (s:Scientist)-[r]->(f:Field)\"\n", @@ -291,10 +436,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "c3d4e5f6-0002-0000-0000-000000000015", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.665256Z", + "iopub.status.busy": "2026-04-19T10:04:37.665176Z", + "iopub.status.idle": "2026-04-19T10:04:37.675994Z", + "shell.execute_reply": "2026-04-19T10:04:37.675294Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "node_props keys: ['Company', 'Field', 'Person', 'Scientist', 'Technology']\n", + "relationships: [{'start': 'Company', 'type': 'ACQUIRED', 'end': 'Company'}, {'start': 'Company', 'type': 'USES', 'end': 'Technology'}, {'start': 'Person', 'type': 'CO_FOUNDED', 'end': 'Company'}, {'start': 'Person', 'type': 'FOUNDED', 'end': 'Company'}, {'start': 'Person', 'type': 'KNOWS', 'end': 'Person'}]\n" + ] + } + ], "source": [ "graph.refresh_schema()\n", "print(\"node_props keys:\", list(graph.structured_schema.get(\"node_props\", {}).keys())[:10])\n", @@ -314,10 +475,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "c3d4e5f6-0002-0000-0000-000000000017", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.677260Z", + "iopub.status.busy": "2026-04-19T10:04:37.677163Z", + "iopub.status.idle": "2026-04-19T10:04:37.699997Z", + "shell.execute_reply": "2026-04-19T10:04:37.699497Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FOUNDED edge count after double upsert (expect 1): 1\n" + ] + } + ], "source": [ "graph.add_graph_documents([doc]) # second upsert — must not create a duplicate edge\n", "cnt = graph.query(\n", @@ -341,10 +517,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "c3d4e5f6-0002-0000-0000-000000000019", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.701586Z", + "iopub.status.busy": "2026-04-19T10:04:37.701456Z", + "iopub.status.idle": "2026-04-19T10:04:37.704265Z", + "shell.execute_reply": "2026-04-19T10:04:37.703710Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.\n" + ] + } + ], "source": [ "if not os.environ.get(\"OPENAI_API_KEY\"):\n", " print(\n", @@ -374,12 +565,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "id": "c3d4e5f6-0002-0000-0000-000000000021", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:37.705737Z", + "iopub.status.busy": "2026-04-19T10:04:37.705643Z", + "iopub.status.idle": "2026-04-19T10:04:37.709913Z", + "shell.execute_reply": "2026-04-19T10:04:37.709405Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cleaned up\n" + ] + } + ], "source": [ - "# 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" + "# 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\".\n", + "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" ] } ], @@ -390,7 +603,16 @@ "name": "python3" }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 5eae0b8..05c5af8 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -33,10 +33,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "d4e5f6a7-0003-0000-0000-000000000003", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:40.114484Z", + "iopub.status.busy": "2026-04-19T10:04:40.114414Z", + "iopub.status.idle": "2026-04-19T10:04:41.329879Z", + "shell.execute_reply": "2026-04-19T10:04:41.329214Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SDK installed\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", + "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" + ] + } + ], "source": [ "import os, sys, subprocess\n", "\n", @@ -129,10 +153,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "d4e5f6a7-0003-0000-0000-000000000005", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.331594Z", + "iopub.status.busy": "2026-04-19T10:04:41.331499Z", + "iopub.status.idle": "2026-04-19T10:04:41.374936Z", + "shell.execute_reply": "2026-04-19T10:04:41.374260Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to 127.0.0.1:37080\n" + ] + } + ], "source": [ "import os, socket\n", "\n", @@ -212,10 +251,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "d4e5f6a7-0003-0000-0000-000000000007", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.376845Z", + "iopub.status.busy": "2026-04-19T10:04:41.376702Z", + "iopub.status.idle": "2026-04-19T10:04:41.600914Z", + "shell.execute_reply": "2026-04-19T10:04:41.600502Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Session: df172441b55146daa9d3efc4ac16db10\n", + "Tools: ['save_fact', 'query_facts', 'find_related', 'list_all_facts']\n" + ] + } + ], "source": [ "import os, re, uuid\n", "from langchain_core.tools import tool\n", @@ -345,10 +400,54 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "d4e5f6a7-0003-0000-0000-000000000009", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.602365Z", + "iopub.status.busy": "2026-04-19T10:04:41.602280Z", + "iopub.status.idle": "2026-04-19T10:04:41.628745Z", + "shell.execute_reply": "2026-04-19T10:04:41.628283Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Saving facts ===\n", + "Saved: Alice -[WORKS_AT]-> Acme Corp\n", + "Saved: Alice -[MANAGES]-> Bob\n", + "Saved: Bob -[WORKS_AT]-> Acme Corp\n", + "Saved: Acme Corp -[LOCATED_IN]-> Berlin\n", + "Saved: Alice -[KNOWS]-> Charlie\n", + "Saved: Charlie -[EXPERT_IN]-> Machine Learning\n", + "\n", + "=== All facts in session ===\n", + "Alice -[KNOWS]-> Charlie\n", + "Alice -[MANAGES]-> Bob\n", + "Alice -[WORKS_AT]-> Acme Corp\n", + "Acme Corp -[LOCATED_IN]-> Berlin\n", + "Bob -[WORKS_AT]-> Acme Corp\n", + "Charlie -[EXPERT_IN]-> Machine Learning\n", + "\n", + "=== Related to Alice (depth=1) ===\n", + "Charlie\n", + "Bob\n", + "Acme Corp\n", + "\n", + "=== Related to Alice (depth=2) ===\n", + "Charlie\n", + "Bob\n", + "Acme Corp\n", + "Machine Learning\n", + "Berlin\n", + "\n", + "=== Cypher query: who works at Acme Corp? ===\n", + "[{'employee': 'Alice'}, {'employee': 'Bob'}]\n" + ] + } + ], "source": [ "ACME_CORP = \"Acme Corp\" # constant used in several save_fact calls below\n", "\n", @@ -391,10 +490,34 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "d4e5f6a7-0003-0000-0000-000000000011", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.630239Z", + "iopub.status.busy": "2026-04-19T10:04:41.630164Z", + "iopub.status.idle": "2026-04-19T10:04:41.715197Z", + "shell.execute_reply": "2026-04-19T10:04:41.714712Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Graph agent output:\n", + " Tell me about Alice\n", + " [extract] Saved: DemoSubject -[DEMO_REL]-> DemoObject\n", + " [query] Facts: Alice -[KNOWS]-> Charlie\n", + "Alice -[MANAGES]-> Bob\n", + "Alice -[WORKS_AT]-> Acme Corp\n", + "Acme Corp -[LOCATED_IN]-> Berlin\n", + "Bob -[WORKS_AT]-> Acme Corp\n", + "Charlie -[EXPERT_IN]-> Machine Learning\n", + "DemoSubject -[DEMO_RE\n" + ] + } + ], "source": [ "from langgraph.graph import StateGraph, END\n", "from typing import TypedDict, Annotated\n", @@ -452,10 +575,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "d4e5f6a7-0003-0000-0000-000000000013", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.716950Z", + "iopub.status.busy": "2026-04-19T10:04:41.716828Z", + "iopub.status.idle": "2026-04-19T10:04:41.719780Z", + "shell.execute_reply": "2026-04-19T10:04:41.719473Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OPENAI_API_KEY not set — skipping LLM agent. See section 2 for the mock demo.\n" + ] + } + ], "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", @@ -491,10 +629,25 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "d4e5f6a7-0003-0000-0000-000000000015", - "metadata": {}, - "outputs": [], + "metadata": { + "execution": { + "iopub.execute_input": "2026-04-19T10:04:41.721097Z", + "iopub.status.busy": "2026-04-19T10:04:41.720992Z", + "iopub.status.idle": "2026-04-19T10:04:41.731850Z", + "shell.execute_reply": "2026-04-19T10:04:41.731455Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cleaned up session: df172441b55146daa9d3efc4ac16db10\n" + ] + } + ], "source": [ "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", "print(\"Cleaned up session:\", SESSION)\n", @@ -509,7 +662,16 @@ "name": "python3" }, "language_info": { - "name": "python" + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docker-compose.yml b/docker-compose.yml index 795b849..a81972a 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.17 + image: ghcr.io/structured-world/coordinode:0.4.1 container_name: coordinode ports: - "7080:7080" # gRPC diff --git a/proto b/proto index e1ab91d..eb472a4 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit e1ab91d180c4b1f61106a09e4b56da6a9736ebdb +Subproject commit eb472a4c6a6069ee4167061442402ceffe42e0cc diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 68f69eb..b7e4fc8 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -18,7 +18,6 @@ AsyncCoordinodeClient, CoordinodeClient, EdgeTypeInfo, - HybridResult, LabelInfo, TextIndexInfo, TextResult, @@ -694,45 +693,3 @@ def test_text_search_fuzzy(client): client.drop_text_index(idx_name) finally: client.cypher(f"MATCH (n:{label} {{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.""" - 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( - 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"] - idx_created = False - try: - client.create_text_index(idx_name, label, "body") - idx_created = True - results = client.hybrid_text_vector_search( - label, - "graph neural", - vec, - limit=5, - ) - assert isinstance(results, list) - if not results: - 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}" - ) - r = results[0] - assert isinstance(r, HybridResult) - assert isinstance(r.node_id, int) - assert isinstance(r.score, float) - assert r.score > 0 - finally: - 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 d0b05dfe0e9162ed8b8d3eb2cad695969677427c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 13:21:59 +0300 Subject: [PATCH 02/25] refactor(sdk): drop orphaned HybridResult and clear notebook outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove HybridResult class and export from coordinode.__init__ — no remaining method returns it after hybrid_text_vector_search() removal (hybrid_search on VectorService returns VectorResult) - Clear execution_count/outputs from all 4 demo notebooks for clean diffs --- coordinode/coordinode/__init__.py | 2 - coordinode/coordinode/client.py | 17 -- demo/notebooks/00_seed_data.ipynb | 125 ++----------- .../01_llama_index_property_graph.ipynb | 166 +++--------------- demo/notebooks/02_langchain_graph_chain.ipynb | 137 ++------------- demo/notebooks/03_langgraph_agent.ipynb | 132 ++------------ 6 files changed, 68 insertions(+), 511 deletions(-) diff --git a/coordinode/coordinode/__init__.py b/coordinode/coordinode/__init__.py index 423f831..c728f12 100644 --- a/coordinode/coordinode/__init__.py +++ b/coordinode/coordinode/__init__.py @@ -23,7 +23,6 @@ CoordinodeClient, EdgeResult, EdgeTypeInfo, - HybridResult, LabelInfo, NodeResult, PropertyDefinitionInfo, @@ -44,7 +43,6 @@ "EdgeResult", "VectorResult", "TextResult", - "HybridResult", "LabelInfo", "EdgeTypeInfo", "PropertyDefinitionInfo", diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index bcbc488..a20f9b8 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -113,23 +113,6 @@ 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 - # NOTE: proto HybridResult carries only node_id + score (no embedded Node - # message). A full node is not included by design — the server returns IDs - # for efficiency. Callers that need node properties should use the client - # API: `client.get_node(self.node_id)`, or match on an application-level - # property in Cypher (e.g. WHERE n.id = ). - - 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).""" diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 707a285..8050a98 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -46,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000003", "metadata": { "execution": { @@ -56,24 +56,7 @@ "shell.execute_reply": "2026-04-19T10:04:06.970069Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ready\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "import os, sys, subprocess\n", "\n", @@ -163,7 +146,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000005", "metadata": { "execution": { @@ -173,15 +156,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.011321Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected to 127.0.0.1:37080\n" - ] - } - ], + "outputs": [], "source": [ "import os, socket\n", "\n", @@ -241,7 +216,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000007", "metadata": { "execution": { @@ -251,16 +226,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.019603Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using DEMO_TAG: seed_data_7cfe797a\n", - "Previous demo data removed\n" - ] - } - ], + "outputs": [], "source": [ "import uuid\n", "\n", @@ -286,7 +252,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000009", "metadata": { "execution": { @@ -296,17 +262,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.056653Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Created 10 people\n", - "Created 6 companies\n", - "Created 8 technologies\n" - ] - } - ], + "outputs": [], "source": [ "# ── People ────────────────────────────────────────────────────────────────\n", "people = [\n", @@ -380,7 +336,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000011", "metadata": { "execution": { @@ -390,15 +346,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.100443Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Created 34 relationships\n" - ] - } - ], + "outputs": [], "source": [ "edges = [\n", " # WORKS_AT\n", @@ -484,7 +432,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000013", "metadata": { "execution": { @@ -494,28 +442,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.109926Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Node counts:\n", - " Person 10\n", - " Company 6\n", - " Technology 8\n", - "\n", - "Relationship counts:\n", - " WORKS_AT 10\n", - " KNOWS 6\n", - " RESEARCHES 6\n", - " USES 5\n", - " BUILDS_ON 4\n", - " FOUNDED 1\n", - " CO_FOUNDED 1\n", - " ACQUIRED 1\n" - ] - } - ], + "outputs": [], "source": [ "from collections import Counter\n", "\n", @@ -540,7 +467,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "a1b2c3d4-0000-0000-0000-000000000014", "metadata": { "execution": { @@ -550,31 +477,7 @@ "shell.execute_reply": "2026-04-19T10:04:07.118356Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Who works at Synthex? ===\n", - " Carol Smith — Founder & CEO\n", - " Eva Müller — Systems Architect\n", - " Henry Rossi — CTO\n", - "\n", - "=== What does Synthex use? ===\n", - " Knowledge Graph\n", - " Vector Database\n", - " RAG\n", - "\n", - "=== GraphRAG dependency chain ===\n", - " → Knowledge Graph\n", - " → RAG\n", - " → Vector Database\n", - "\n", - "✓ Demo data seeded.\n", - "To query it from notebooks 01–03, connect them to the same CoordiNode server (COORDINODE_ADDR).\n" - ] - } - ], + "outputs": [], "source": [ "print(\"=== Who works at Synthex? ===\")\n", "rows = client.cypher(\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index e24649a..1970d97 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000003", "metadata": { "execution": { @@ -45,24 +45,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.114595Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SDK installed\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "import os, sys, subprocess\n", "\n", @@ -157,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000005", "metadata": { "execution": { @@ -205,7 +188,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000007", "metadata": { "execution": { @@ -215,15 +198,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.161134Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected to 127.0.0.1:37080\n" - ] - } - ], + "outputs": [], "source": [ "import os, socket\n", "\n", @@ -289,7 +264,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000009", "metadata": { "execution": { @@ -299,29 +274,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.760858Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected. Schema:\n", - "Node labels:\n", - " - Company\n", - " - Person\n", - " - Technology\n", - "\n", - "Edge types:\n", - " - ACQUIRED\n", - " - BUILDS_ON\n", - " - CO_FOUNDED\n", - " - FOUNDED\n", - " - KNOWS\n", - " - RESEARCHES\n", - " - USES\n", - " - WORKS_AT\n" - ] - } - ], + "outputs": [], "source": [ "from llama_index.graph_stores.coordinode import CoordinodePropertyGraphStore\n", "from llama_index.core.graph_stores.types import EntityNode, Relation\n", @@ -343,7 +296,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000011", "metadata": { "execution": { @@ -353,16 +306,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.773770Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Upserted nodes: ['Alice-590af6188388463ba7e3f958c5d81a36', 'Bob-590af6188388463ba7e3f958c5d81a36', 'GraphRAG-590af6188388463ba7e3f958c5d81a36']\n", - "Upserted relations: ['RESEARCHES', 'COLLABORATES', 'IMPLEMENTS']\n" - ] - } - ], + "outputs": [], "source": [ "import uuid\n", "\n", @@ -396,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000013", "metadata": { "execution": { @@ -406,17 +350,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.781790Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Triplets for Alice-590af6188388463ba7e3f958c5d81a36:\n", - " Alice-590af6188388463ba7e3f958c5d81a36 --[COLLABORATES]--> Bob-590af6188388463ba7e3f958c5d81a36\n", - " Alice-590af6188388463ba7e3f958c5d81a36 --[RESEARCHES]--> GraphRAG-590af6188388463ba7e3f958c5d81a36\n" - ] - } - ], + "outputs": [], "source": [ "triplets = store.get_triplets(entity_names=[f\"Alice-{tag}\"])\n", "print(f\"Triplets for Alice-{tag}:\")\n", @@ -434,7 +368,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000015", "metadata": { "execution": { @@ -444,17 +378,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.792155Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Rel map for Alice-590af6188388463ba7e3f958c5d81a36 (2 rows):\n", - " Alice-590af6188388463ba7e3f958c5d81a36 --[COLLABORATES]--> Bob-590af6188388463ba7e3f958c5d81a36\n", - " Alice-590af6188388463ba7e3f958c5d81a36 --[RESEARCHES]--> GraphRAG-590af6188388463ba7e3f958c5d81a36\n" - ] - } - ], + "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", @@ -473,7 +397,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000017", "metadata": { "execution": { @@ -483,15 +407,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.797951Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Query result: [{'person': 'Alice-590af6188388463ba7e3f958c5d81a36', 'since': None, 'topic': 'GraphRAG-590af6188388463ba7e3f958c5d81a36'}]\n" - ] - } - ], + "outputs": [], "source": [ "rows = store.structured_query(\n", " \"MATCH (p:Person)-[r:RESEARCHES]->(t:Topic)\"\n", @@ -512,7 +428,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000019", "metadata": { "execution": { @@ -522,31 +438,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.805001Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Node labels:\n", - " - Company\n", - " - Person\n", - " - Technology\n", - " - Topic\n", - "\n", - "Edge types:\n", - " - ACQUIRED\n", - " - BUILDS_ON\n", - " - COLLABORATES\n", - " - CO_FOUNDED\n", - " - FOUNDED\n", - " - IMPLEMENTS\n", - " - KNOWS\n", - " - RESEARCHES\n", - " - USES\n", - " - WORKS_AT\n" - ] - } - ], + "outputs": [], "source": [ "schema = store.get_schema()\n", "print(schema)" @@ -562,7 +454,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000021", "metadata": { "execution": { @@ -572,15 +464,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.822393Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Edge count after double upsert (expect 1): 1\n" - ] - } - ], + "outputs": [], "source": [ "store.upsert_relations(relations) # second call — should still be exactly 1 edge\n", "rows = store.structured_query(\n", @@ -600,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "b2c3d4e5-0001-0000-0000-000000000023", "metadata": { "execution": { @@ -610,15 +494,7 @@ "shell.execute_reply": "2026-04-19T10:04:15.839478Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cleaned up\n" - ] - } - ], + "outputs": [], "source": [ "store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n", "print(\"Cleaned up\")\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index c4d1c14..87812df 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000003", "metadata": { "execution": { @@ -42,24 +42,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.524144Z" } }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SDK installed\n" - ] - } - ], + "outputs": [], "source": [ "import os, sys, subprocess\n", "\n", @@ -158,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000005", "metadata": { "execution": { @@ -206,7 +189,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000007", "metadata": { "execution": { @@ -216,15 +199,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.573499Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected to 127.0.0.1:37080\n" - ] - } - ], + "outputs": [], "source": [ "import os, socket\n", "\n", @@ -288,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000009", "metadata": { "execution": { @@ -298,31 +273,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.645347Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected. Schema preview:\n", - "Node labels:\n", - " - Company\n", - " - Person\n", - " - Technology\n", - "\n", - "Edge types:\n", - " - ACQUIRED\n", - " - BUILDS_ON\n", - " - COLLABORATES\n", - " - CO_FOUNDED\n", - " - FOUNDED\n", - " - IMPLEMENTS\n", - " - KNOWS\n", - " - RESEARCHES\n", - " - USES\n", - " - WORKS_AT\n" - ] - } - ], + "outputs": [], "source": [ "import os, uuid\n", "from langchain_coordinode import CoordinodeGraph\n", @@ -347,7 +298,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000011", "metadata": { "execution": { @@ -357,15 +308,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.658042Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Documents added\n" - ] - } - ], + "outputs": [], "source": [ "tag = uuid.uuid4().hex\n", "\n", @@ -394,7 +337,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000013", "metadata": { "execution": { @@ -404,16 +347,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.663378Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Scientists → Fields:\n", - " Turing-a0a7f80c9ed8491d818da73110a9b2fa --[FOUNDED]--> Cryptography-a0a7f80c9ed8491d818da73110a9b2fa\n" - ] - } - ], + "outputs": [], "source": [ "rows = graph.query(\n", " \"MATCH (s:Scientist)-[r]->(f:Field)\"\n", @@ -436,7 +370,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000015", "metadata": { "execution": { @@ -446,16 +380,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.675294Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "node_props keys: ['Company', 'Field', 'Person', 'Scientist', 'Technology']\n", - "relationships: [{'start': 'Company', 'type': 'ACQUIRED', 'end': 'Company'}, {'start': 'Company', 'type': 'USES', 'end': 'Technology'}, {'start': 'Person', 'type': 'CO_FOUNDED', 'end': 'Company'}, {'start': 'Person', 'type': 'FOUNDED', 'end': 'Company'}, {'start': 'Person', 'type': 'KNOWS', 'end': 'Person'}]\n" - ] - } - ], + "outputs": [], "source": [ "graph.refresh_schema()\n", "print(\"node_props keys:\", list(graph.structured_schema.get(\"node_props\", {}).keys())[:10])\n", @@ -475,7 +400,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000017", "metadata": { "execution": { @@ -485,15 +410,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.699497Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "FOUNDED edge count after double upsert (expect 1): 1\n" - ] - } - ], + "outputs": [], "source": [ "graph.add_graph_documents([doc]) # second upsert — must not create a duplicate edge\n", "cnt = graph.query(\n", @@ -517,7 +434,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000019", "metadata": { "execution": { @@ -527,15 +444,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.703710Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Skipping: OPENAI_API_KEY is not set. Set it via os.environ[\"OPENAI_API_KEY\"] = \"sk-...\" and re-run this cell.\n" - ] - } - ], + "outputs": [], "source": [ "if not os.environ.get(\"OPENAI_API_KEY\"):\n", " print(\n", @@ -565,7 +474,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "c3d4e5f6-0002-0000-0000-000000000021", "metadata": { "execution": { @@ -575,15 +484,7 @@ "shell.execute_reply": "2026-04-19T10:04:37.709405Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cleaned up\n" - ] - } - ], + "outputs": [], "source": [ "# 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", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 05c5af8..cf1d213 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000003", "metadata": { "execution": { @@ -43,24 +43,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.329214Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SDK installed\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m24.0\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m26.0.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip3 install --upgrade pip\u001b[0m\n" - ] - } - ], + "outputs": [], "source": [ "import os, sys, subprocess\n", "\n", @@ -153,7 +136,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000005", "metadata": { "execution": { @@ -163,15 +146,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.374260Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connected to 127.0.0.1:37080\n" - ] - } - ], + "outputs": [], "source": [ "import os, socket\n", "\n", @@ -251,7 +226,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000007", "metadata": { "execution": { @@ -261,16 +236,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.600502Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Session: df172441b55146daa9d3efc4ac16db10\n", - "Tools: ['save_fact', 'query_facts', 'find_related', 'list_all_facts']\n" - ] - } - ], + "outputs": [], "source": [ "import os, re, uuid\n", "from langchain_core.tools import tool\n", @@ -400,7 +366,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000009", "metadata": { "execution": { @@ -410,44 +376,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.628283Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== Saving facts ===\n", - "Saved: Alice -[WORKS_AT]-> Acme Corp\n", - "Saved: Alice -[MANAGES]-> Bob\n", - "Saved: Bob -[WORKS_AT]-> Acme Corp\n", - "Saved: Acme Corp -[LOCATED_IN]-> Berlin\n", - "Saved: Alice -[KNOWS]-> Charlie\n", - "Saved: Charlie -[EXPERT_IN]-> Machine Learning\n", - "\n", - "=== All facts in session ===\n", - "Alice -[KNOWS]-> Charlie\n", - "Alice -[MANAGES]-> Bob\n", - "Alice -[WORKS_AT]-> Acme Corp\n", - "Acme Corp -[LOCATED_IN]-> Berlin\n", - "Bob -[WORKS_AT]-> Acme Corp\n", - "Charlie -[EXPERT_IN]-> Machine Learning\n", - "\n", - "=== Related to Alice (depth=1) ===\n", - "Charlie\n", - "Bob\n", - "Acme Corp\n", - "\n", - "=== Related to Alice (depth=2) ===\n", - "Charlie\n", - "Bob\n", - "Acme Corp\n", - "Machine Learning\n", - "Berlin\n", - "\n", - "=== Cypher query: who works at Acme Corp? ===\n", - "[{'employee': 'Alice'}, {'employee': 'Bob'}]\n" - ] - } - ], + "outputs": [], "source": [ "ACME_CORP = \"Acme Corp\" # constant used in several save_fact calls below\n", "\n", @@ -490,7 +419,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000011", "metadata": { "execution": { @@ -500,24 +429,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.714712Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Graph agent output:\n", - " Tell me about Alice\n", - " [extract] Saved: DemoSubject -[DEMO_REL]-> DemoObject\n", - " [query] Facts: Alice -[KNOWS]-> Charlie\n", - "Alice -[MANAGES]-> Bob\n", - "Alice -[WORKS_AT]-> Acme Corp\n", - "Acme Corp -[LOCATED_IN]-> Berlin\n", - "Bob -[WORKS_AT]-> Acme Corp\n", - "Charlie -[EXPERT_IN]-> Machine Learning\n", - "DemoSubject -[DEMO_RE\n" - ] - } - ], + "outputs": [], "source": [ "from langgraph.graph import StateGraph, END\n", "from typing import TypedDict, Annotated\n", @@ -575,7 +487,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000013", "metadata": { "execution": { @@ -585,15 +497,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.719473Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "OPENAI_API_KEY not set — skipping LLM agent. See section 2 for the mock demo.\n" - ] - } - ], + "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", @@ -629,7 +533,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "d4e5f6a7-0003-0000-0000-000000000015", "metadata": { "execution": { @@ -639,15 +543,7 @@ "shell.execute_reply": "2026-04-19T10:04:41.731455Z" } }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cleaned up session: df172441b55146daa9d3efc4ac16db10\n" - ] - } - ], + "outputs": [], "source": [ "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", "print(\"Cleaned up session:\", SESSION)\n", From c2ce32064dd7f6495f0998049c0cc2f6f7a5767d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 14:58:34 +0300 Subject: [PATCH 03/25] chore(demo): strip non-deterministic notebook metadata Remove per-cell execution.* timestamps and trim language_info down to {name: python}. Prevents diff noise from exact Python patch versions, UUIDs, and run timestamps on notebook re-execution. --- demo/notebooks/00_seed_data.ipynb | 158 +++++--------- .../01_llama_index_property_graph.ipynb | 200 +++++------------- demo/notebooks/02_langchain_graph_chain.ipynb | 183 +++++----------- demo/notebooks/03_langgraph_agent.ipynb | 150 ++++--------- 4 files changed, 205 insertions(+), 486 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 8050a98..36fe1a6 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000001", + "id": "0", "metadata": {}, "source": [ "# Seed Demo Data\n", @@ -12,7 +12,7 @@ "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", + "> lives only inside this notebook process \u2014 notebooks 01\u201303 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", @@ -20,17 +20,17 @@ "- 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", + "- ~35 relationships (WORKS_AT, FOUNDED, KNOWS, RESEARCHES, INVENTED, ACQUIRED, USES, \u2026)\n", "\n", "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", "**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", + "- **Google Colab** \u2014 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** \u2014 connects to a running CoordiNode server via gRPC.\n", "\n", - "> **⚠️ Note for real-server use:** All writes and the cleanup step are scoped to `demo_tag`.\n", + "> **\u26a0\ufe0f 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." @@ -38,7 +38,7 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000002", + "id": "1", "metadata": {}, "source": [ "## Install dependencies" @@ -47,15 +47,8 @@ { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000003", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:06.039243Z", - "iopub.status.busy": "2026-04-19T10:04:06.039172Z", - "iopub.status.idle": "2026-04-19T10:04:06.970761Z", - "shell.execute_reply": "2026-04-19T10:04:06.970069Z" - } - }, + "id": "2", + "metadata": {}, "outputs": [], "source": [ "import os, sys, subprocess\n", @@ -63,12 +56,12 @@ "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 COORDINODE_ADDR is set, a live server is already available \u2014 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", + " # Colab's apt packages ship rustc \u22641.75, which cannot build coordinode-embedded\n", + " # (requires Rust \u22651.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly \u2014 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", @@ -81,7 +74,7 @@ " # never runs when a live gRPC server is available, so there is no risk of\n", " # unintentional execution in local or server environments.\n", " # Security note: downloading rustup-init via HTTPS with cert verification and\n", - " # executing from a temp file (not piped to shell) is by design — this is the\n", + " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", @@ -134,12 +127,12 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000004", + "id": "3", "metadata": {}, "source": [ "## Connect to CoordiNode\n", "\n", - "- **Colab**: uses `LocalClient(\":memory:\")` — in-process embedded engine, no server required.\n", + "- **Colab**: uses `LocalClient(\":memory:\")` \u2014 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." ] @@ -147,15 +140,8 @@ { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000005", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:06.972780Z", - "iopub.status.busy": "2026-04-19T10:04:06.972654Z", - "iopub.status.idle": "2026-04-19T10:04:07.012113Z", - "shell.execute_reply": "2026-04-19T10:04:07.011321Z" - } - }, + "id": "4", + "metadata": {}, "outputs": [], "source": [ "import os, socket\n", @@ -192,14 +178,14 @@ " 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", + " # No server available \u2014 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@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", - " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " client = LocalClient(\":memory:\")\n", @@ -208,24 +194,17 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000006", + "id": "5", "metadata": {}, "source": [ - "## Step 1 — Clear previous demo data" + "## Step 1 \u2014 Clear previous demo data" ] }, { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000007", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:07.014105Z", - "iopub.status.busy": "2026-04-19T10:04:07.013909Z", - "iopub.status.idle": "2026-04-19T10:04:07.020088Z", - "shell.execute_reply": "2026-04-19T10:04:07.019603Z" - } - }, + "id": "6", + "metadata": {}, "outputs": [], "source": [ "import uuid\n", @@ -234,7 +213,7 @@ "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", + "# each edge twice \u2014 once per endpoint \u2014 causing duplicate-delete errors).\n", "client.cypher(\n", " \"MATCH (n {demo: true, demo_tag: $tag}) DETACH DELETE n\",\n", " params={\"tag\": DEMO_TAG},\n", @@ -244,33 +223,26 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000008", + "id": "7", "metadata": {}, "source": [ - "## Step 2 — Create nodes" + "## Step 2 \u2014 Create nodes" ] }, { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000009", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:07.021785Z", - "iopub.status.busy": "2026-04-19T10:04:07.021661Z", - "iopub.status.idle": "2026-04-19T10:04:07.057106Z", - "shell.execute_reply": "2026-04-19T10:04:07.056653Z" - } - }, + "id": "8", + "metadata": {}, "outputs": [], "source": [ - "# ── People ────────────────────────────────────────────────────────────────\n", + "# \u2500\u2500 People \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\": \"Eva M\u00fcller\", \"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", @@ -287,7 +259,7 @@ "\n", "print(f\"Created {len(people)} people\")\n", "\n", - "# ── Companies ─────────────────────────────────────────────────────────────\n", + "# \u2500\u2500 Companies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "companies = [\n", " {\"name\": \"Google\", \"industry\": \"Technology\", \"founded\": 1998, \"hq\": \"Mountain View\"},\n", " {\"name\": \"Meta\", \"industry\": \"Technology\", \"founded\": 2004, \"hq\": \"Menlo Park\"},\n", @@ -305,7 +277,7 @@ "\n", "print(f\"Created {len(companies)} companies\")\n", "\n", - "# ── Technologies ──────────────────────────────────────────────────────────\n", + "# \u2500\u2500 Technologies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n", "technologies = [\n", " {\"name\": \"Transformer\", \"type\": \"Architecture\", \"year\": 2017},\n", " {\"name\": \"Graph Neural Network\", \"type\": \"Algorithm\", \"year\": 2009},\n", @@ -328,24 +300,17 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000010", + "id": "9", "metadata": {}, "source": [ - "## Step 3 — Create relationships" + "## Step 3 \u2014 Create relationships" ] }, { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000011", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:07.058969Z", - "iopub.status.busy": "2026-04-19T10:04:07.058874Z", - "iopub.status.idle": "2026-04-19T10:04:07.100906Z", - "shell.execute_reply": "2026-04-19T10:04:07.100443Z" - } - }, + "id": "10", + "metadata": {}, "outputs": [], "source": [ "edges = [\n", @@ -354,7 +319,7 @@ " (\"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", + " (\"Eva M\u00fcller\", \"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", @@ -369,7 +334,7 @@ " (\"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", + " (\"Eva M\u00fcller\", \"KNOWS\", \"Grace Okafor\", {}),\n", " # RESEARCHES / WORKS_ON\n", " (\"Alice Chen\", \"RESEARCHES\", \"Reinforcement Learning\", {\"since\": 2019}),\n", " (\"David Park\", \"RESEARCHES\", \"LLM\", {\"since\": 2020}),\n", @@ -424,24 +389,17 @@ }, { "cell_type": "markdown", - "id": "a1b2c3d4-0000-0000-0000-000000000012", + "id": "11", "metadata": {}, "source": [ - "## Step 4 — Verify" + "## Step 4 \u2014 Verify" ] }, { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000013", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:07.102456Z", - "iopub.status.busy": "2026-04-19T10:04:07.102333Z", - "iopub.status.idle": "2026-04-19T10:04:07.110337Z", - "shell.execute_reply": "2026-04-19T10:04:07.109926Z" - } - }, + "id": "12", + "metadata": {}, "outputs": [], "source": [ "from collections import Counter\n", @@ -468,15 +426,8 @@ { "cell_type": "code", "execution_count": null, - "id": "a1b2c3d4-0000-0000-0000-000000000014", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:07.112104Z", - "iopub.status.busy": "2026-04-19T10:04:07.111970Z", - "iopub.status.idle": "2026-04-19T10:04:07.118744Z", - "shell.execute_reply": "2026-04-19T10:04:07.118356Z" - } - }, + "id": "13", + "metadata": {}, "outputs": [], "source": [ "print(\"=== Who works at Synthex? ===\")\n", @@ -486,7 +437,7 @@ " params={\"co\": \"Synthex\", \"tag\": DEMO_TAG},\n", ")\n", "for r in rows:\n", - " print(f\" {r['name']} — {r['role']}\")\n", + " print(f\" {r['name']} \u2014 {r['role']}\")\n", "\n", "print(\"\\n=== What does Synthex use? ===\")\n", "rows = client.cypher(\n", @@ -502,10 +453,10 @@ " params={\"tech\": \"GraphRAG\", \"tag\": DEMO_TAG},\n", ")\n", "for r in rows:\n", - " print(f\" → {r['dependency']}\")\n", + " print(f\" \u2192 {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", + "print(\"\\n\u2713 Demo data seeded.\")\n", + "print(\"To query it from notebooks 01\u201303, connect them to the same CoordiNode server (COORDINODE_ADDR).\")\n", "client.close()" ] } @@ -517,16 +468,7 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" + "name": "python" } }, "nbformat": 4, diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 1970d97..c005c57 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000001", + "id": "0", "metadata": {}, "source": [ "# LlamaIndex + CoordiNode: PropertyGraphIndex\n", @@ -12,22 +12,22 @@ "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", + "- `upsert_nodes` / `upsert_relations` \u2014 idempotent MERGE (safe to call multiple times)\n", + "- `get()` \u2014 look up nodes by ID or properties\n", + "- `get_triplets()` \u2014 all edges (wildcard) or filtered by relation type / entity name\n", + "- `get_rel_map()` \u2014 outgoing relations for a set of nodes (depth=1)\n", + "- `structured_query()` \u2014 arbitrary Cypher pass-through\n", + "- `delete()` \u2014 remove nodes by id or name\n", + "- `get_schema()` \u2014 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." + "- **Google Colab** \u2014 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** \u2014 connects to a running CoordiNode server via gRPC." ] }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000002", + "id": "1", "metadata": {}, "source": [ "## Install dependencies" @@ -36,15 +36,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000003", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:13.854777Z", - "iopub.status.busy": "2026-04-19T10:04:13.854705Z", - "iopub.status.idle": "2026-04-19T10:04:15.115115Z", - "shell.execute_reply": "2026-04-19T10:04:15.114595Z" - } - }, + "id": "2", + "metadata": {}, "outputs": [], "source": [ "import os, sys, subprocess\n", @@ -54,9 +47,9 @@ "# 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", + " # Colab's apt packages ship rustc \u22641.75, which cannot build coordinode-embedded\n", + " # (requires Rust \u22651.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly \u2014 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", @@ -69,7 +62,7 @@ " # Colab sessions, so there is no risk of unintentional execution in local\n", " # or server environments.\n", " # Security note: downloading rustup-init via HTTPS with cert verification and\n", - " # executing from a temp file (not piped to shell) is by design — this is the\n", + " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", @@ -128,7 +121,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000004", + "id": "3", "metadata": {}, "source": [ "## Adapter for embedded mode\n", @@ -141,15 +134,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000005", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.116903Z", - "iopub.status.busy": "2026-04-19T10:04:15.116806Z", - "iopub.status.idle": "2026-04-19T10:04:15.120140Z", - "shell.execute_reply": "2026-04-19T10:04:15.119700Z" - } - }, + "id": "4", + "metadata": {}, "outputs": [], "source": [ "class _EmbeddedAdapter:\n", @@ -172,7 +158,7 @@ " lines.append(f\" - {r['t']}\")\n", " return \"\\n\".join(lines)\n", "\n", - " # Vector search not available in embedded mode — requires running CoordiNode server.\n", + " # Vector search not available in embedded mode \u2014 requires running CoordiNode server.\n", "\n", " def close(self):\n", " self._lc.close()\n" @@ -180,7 +166,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000006", + "id": "5", "metadata": {}, "source": [ "## Connect to CoordiNode" @@ -189,15 +175,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000007", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.121236Z", - "iopub.status.busy": "2026-04-19T10:04:15.121171Z", - "iopub.status.idle": "2026-04-19T10:04:15.161579Z", - "shell.execute_reply": "2026-04-19T10:04:15.161134Z" - } - }, + "id": "6", + "metadata": {}, "outputs": [], "source": [ "import os, socket\n", @@ -236,7 +215,7 @@ " 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", + " # No server available \u2014 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", @@ -244,7 +223,7 @@ " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\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", + " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " _lc = LocalClient(\":memory:\")\n", @@ -254,7 +233,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000008", + "id": "7", "metadata": {}, "source": [ "## Create the property graph store\n", @@ -265,15 +244,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000009", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.163153Z", - "iopub.status.busy": "2026-04-19T10:04:15.162973Z", - "iopub.status.idle": "2026-04-19T10:04:15.761333Z", - "shell.execute_reply": "2026-04-19T10:04:15.760858Z" - } - }, + "id": "8", + "metadata": {}, "outputs": [], "source": [ "from llama_index.graph_stores.coordinode import CoordinodePropertyGraphStore\n", @@ -286,7 +258,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000010", + "id": "9", "metadata": {}, "source": [ "## 1. Upsert nodes and relations\n", @@ -297,15 +269,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000011", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.762887Z", - "iopub.status.busy": "2026-04-19T10:04:15.762722Z", - "iopub.status.idle": "2026-04-19T10:04:15.774422Z", - "shell.execute_reply": "2026-04-19T10:04:15.773770Z" - } - }, + "id": "10", + "metadata": {}, "outputs": [], "source": [ "import uuid\n", @@ -332,24 +297,17 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000012", + "id": "11", "metadata": {}, "source": [ - "## 2. get_triplets — all edges from a node (wildcard)" + "## 2. get_triplets \u2014 all edges from a node (wildcard)" ] }, { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000013", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.775786Z", - "iopub.status.busy": "2026-04-19T10:04:15.775698Z", - "iopub.status.idle": "2026-04-19T10:04:15.782234Z", - "shell.execute_reply": "2026-04-19T10:04:15.781790Z" - } - }, + "id": "12", + "metadata": {}, "outputs": [], "source": [ "triplets = store.get_triplets(entity_names=[f\"Alice-{tag}\"])\n", @@ -360,24 +318,17 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000014", + "id": "13", "metadata": {}, "source": [ - "## 3. get_rel_map — relations for a set of nodes" + "## 3. get_rel_map \u2014 relations for a set of nodes" ] }, { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000015", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.784083Z", - "iopub.status.busy": "2026-04-19T10:04:15.783972Z", - "iopub.status.idle": "2026-04-19T10:04:15.792506Z", - "shell.execute_reply": "2026-04-19T10:04:15.792155Z" - } - }, + "id": "14", + "metadata": {}, "outputs": [], "source": [ "found_alice = store.get(properties={\"name\": f\"Alice-{tag}\"})\n", @@ -389,24 +340,17 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000016", + "id": "15", "metadata": {}, "source": [ - "## 4. structured_query — arbitrary Cypher" + "## 4. structured_query \u2014 arbitrary Cypher" ] }, { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000017", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.793712Z", - "iopub.status.busy": "2026-04-19T10:04:15.793643Z", - "iopub.status.idle": "2026-04-19T10:04:15.798524Z", - "shell.execute_reply": "2026-04-19T10:04:15.797951Z" - } - }, + "id": "16", + "metadata": {}, "outputs": [], "source": [ "rows = store.structured_query(\n", @@ -420,7 +364,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000018", + "id": "17", "metadata": {}, "source": [ "## 5. get_schema" @@ -429,15 +373,8 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000019", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.800392Z", - "iopub.status.busy": "2026-04-19T10:04:15.800302Z", - "iopub.status.idle": "2026-04-19T10:04:15.805441Z", - "shell.execute_reply": "2026-04-19T10:04:15.805001Z" - } - }, + "id": "18", + "metadata": {}, "outputs": [], "source": [ "schema = store.get_schema()\n", @@ -446,27 +383,20 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000020", + "id": "19", "metadata": {}, "source": [ - "## 6. Idempotency — double upsert must not duplicate edges" + "## 6. Idempotency \u2014 double upsert must not duplicate edges" ] }, { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000021", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.806831Z", - "iopub.status.busy": "2026-04-19T10:04:15.806753Z", - "iopub.status.idle": "2026-04-19T10:04:15.823143Z", - "shell.execute_reply": "2026-04-19T10:04:15.822393Z" - } - }, + "id": "20", + "metadata": {}, "outputs": [], "source": [ - "store.upsert_relations(relations) # second call — should still be exactly 1 edge\n", + "store.upsert_relations(relations) # second call \u2014 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", @@ -476,7 +406,7 @@ }, { "cell_type": "markdown", - "id": "b2c3d4e5-0001-0000-0000-000000000022", + "id": "21", "metadata": {}, "source": [ "## Cleanup" @@ -485,21 +415,14 @@ { "cell_type": "code", "execution_count": null, - "id": "b2c3d4e5-0001-0000-0000-000000000023", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:15.824642Z", - "iopub.status.busy": "2026-04-19T10:04:15.824537Z", - "iopub.status.idle": "2026-04-19T10:04:15.840013Z", - "shell.execute_reply": "2026-04-19T10:04:15.839478Z" - } - }, + "id": "22", + "metadata": {}, "outputs": [], "source": [ "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 caller" + "client.close() # injected client \u2014 owned by caller" ] } ], @@ -510,16 +433,7 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" + "name": "python" } }, "nbformat": 4, diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 87812df..97daaed 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000001", + "id": "0", "metadata": {}, "source": [ "# LangChain + CoordiNode: Graph Chain\n", @@ -12,19 +12,19 @@ "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", + "- `graph.query()` \u2014 arbitrary Cypher pass-through\n", + "- `graph.schema` / `refresh_schema()` \u2014 live graph schema\n", + "- `add_graph_documents()` \u2014 add Nodes + Relationships from a LangChain `GraphDocument`\n", + "- `GraphCypherQAChain` \u2014 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." + "- **Google Colab** \u2014 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** \u2014 connects to a running CoordiNode server via gRPC." ] }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000002", + "id": "1", "metadata": {}, "source": [ "## Install dependencies" @@ -33,15 +33,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000003", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:18.638106Z", - "iopub.status.busy": "2026-04-19T10:04:18.638034Z", - "iopub.status.idle": "2026-04-19T10:04:37.524798Z", - "shell.execute_reply": "2026-04-19T10:04:37.524144Z" - } - }, + "id": "2", + "metadata": {}, "outputs": [], "source": [ "import os, sys, subprocess\n", @@ -51,9 +44,9 @@ "# 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", + " # Colab's apt packages ship rustc \u22641.75, which cannot build coordinode-embedded\n", + " # (requires Rust \u22651.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly \u2014 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", @@ -66,7 +59,7 @@ " # Colab sessions, so there is no risk of unintentional execution in local\n", " # or server environments.\n", " # Security note: downloading rustup-init via HTTPS with cert verification and\n", - " # executing from a temp file (not piped to shell) is by design — this is the\n", + " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", @@ -129,7 +122,7 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000004", + "id": "3", "metadata": {}, "source": [ "## Adapter for embedded mode\n", @@ -142,15 +135,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000005", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.526604Z", - "iopub.status.busy": "2026-04-19T10:04:37.526462Z", - "iopub.status.idle": "2026-04-19T10:04:37.529848Z", - "shell.execute_reply": "2026-04-19T10:04:37.529456Z" - } - }, + "id": "4", + "metadata": {}, "outputs": [], "source": [ "class _EmbeddedAdapter:\n", @@ -173,7 +159,7 @@ " lines.append(f\" - {r['t']}\")\n", " return \"\\n\".join(lines)\n", "\n", - " # Vector search not available in embedded mode — requires running CoordiNode server.\n", + " # Vector search not available in embedded mode \u2014 requires running CoordiNode server.\n", "\n", " def close(self):\n", " self._lc.close()\n" @@ -181,7 +167,7 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000006", + "id": "5", "metadata": {}, "source": [ "## Connect to CoordiNode" @@ -190,15 +176,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000007", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.531142Z", - "iopub.status.busy": "2026-04-19T10:04:37.531071Z", - "iopub.status.idle": "2026-04-19T10:04:37.574066Z", - "shell.execute_reply": "2026-04-19T10:04:37.573499Z" - } - }, + "id": "6", + "metadata": {}, "outputs": [], "source": [ "import os, socket\n", @@ -235,7 +214,7 @@ " 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", + " # No server available \u2014 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", @@ -243,7 +222,7 @@ " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\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", + " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " _lc = LocalClient(\":memory:\")\n", @@ -253,7 +232,7 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000008", + "id": "7", "metadata": {}, "source": [ "## Create the graph store\n", @@ -264,15 +243,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000009", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.575712Z", - "iopub.status.busy": "2026-04-19T10:04:37.575560Z", - "iopub.status.idle": "2026-04-19T10:04:37.645810Z", - "shell.execute_reply": "2026-04-19T10:04:37.645347Z" - } - }, + "id": "8", + "metadata": {}, "outputs": [], "source": [ "import os, uuid\n", @@ -287,7 +259,7 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000010", + "id": "9", "metadata": {}, "source": [ "## 1. add_graph_documents\n", @@ -299,15 +271,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000011", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.647188Z", - "iopub.status.busy": "2026-04-19T10:04:37.647116Z", - "iopub.status.idle": "2026-04-19T10:04:37.658568Z", - "shell.execute_reply": "2026-04-19T10:04:37.658042Z" - } - }, + "id": "10", + "metadata": {}, "outputs": [], "source": [ "tag = uuid.uuid4().hex\n", @@ -329,24 +294,17 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000012", + "id": "11", "metadata": {}, "source": [ - "## 2. query — direct Cypher" + "## 2. query \u2014 direct Cypher" ] }, { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000013", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.660044Z", - "iopub.status.busy": "2026-04-19T10:04:37.659935Z", - "iopub.status.idle": "2026-04-19T10:04:37.663799Z", - "shell.execute_reply": "2026-04-19T10:04:37.663378Z" - } - }, + "id": "12", + "metadata": {}, "outputs": [], "source": [ "rows = graph.query(\n", @@ -355,31 +313,24 @@ " \" RETURN s.name AS scientist, type(r) AS relation, f.name AS field\",\n", " params={\"prefix\": f\"Turing-{tag}\"},\n", ")\n", - "print(\"Scientists → Fields:\")\n", + "print(\"Scientists \u2192 Fields:\")\n", "for r in rows:\n", " print(f\" {r['scientist']} --[{r['relation']}]--> {r['field']}\")" ] }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000014", + "id": "13", "metadata": {}, "source": [ - "## 3. refresh_schema — structured_schema dict" + "## 3. refresh_schema \u2014 structured_schema dict" ] }, { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000015", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.665256Z", - "iopub.status.busy": "2026-04-19T10:04:37.665176Z", - "iopub.status.idle": "2026-04-19T10:04:37.675994Z", - "shell.execute_reply": "2026-04-19T10:04:37.675294Z" - } - }, + "id": "14", + "metadata": {}, "outputs": [], "source": [ "graph.refresh_schema()\n", @@ -389,30 +340,23 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000016", + "id": "15", "metadata": {}, "source": [ "## 4. Idempotency check\n", "\n", - "`add_graph_documents` uses MERGE internally — adding the same document twice must not\n", + "`add_graph_documents` uses MERGE internally \u2014 adding the same document twice must not\n", "create duplicate edges." ] }, { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000017", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.677260Z", - "iopub.status.busy": "2026-04-19T10:04:37.677163Z", - "iopub.status.idle": "2026-04-19T10:04:37.699997Z", - "shell.execute_reply": "2026-04-19T10:04:37.699497Z" - } - }, + "id": "16", + "metadata": {}, "outputs": [], "source": [ - "graph.add_graph_documents([doc]) # second upsert — must not create a duplicate edge\n", + "graph.add_graph_documents([doc]) # second upsert \u2014 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", @@ -422,10 +366,10 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000018", + "id": "17", "metadata": {}, "source": [ - "## 5. GraphCypherQAChain — LLM-powered Cypher (optional)\n", + "## 5. GraphCypherQAChain \u2014 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", @@ -435,15 +379,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000019", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.701586Z", - "iopub.status.busy": "2026-04-19T10:04:37.701456Z", - "iopub.status.idle": "2026-04-19T10:04:37.704265Z", - "shell.execute_reply": "2026-04-19T10:04:37.703710Z" - } - }, + "id": "18", + "metadata": {}, "outputs": [], "source": [ "if not os.environ.get(\"OPENAI_API_KEY\"):\n", @@ -466,7 +403,7 @@ }, { "cell_type": "markdown", - "id": "c3d4e5f6-0002-0000-0000-000000000020", + "id": "19", "metadata": {}, "source": [ "## Cleanup" @@ -475,15 +412,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c3d4e5f6-0002-0000-0000-000000000021", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:37.705737Z", - "iopub.status.busy": "2026-04-19T10:04:37.705643Z", - "iopub.status.idle": "2026-04-19T10:04:37.709913Z", - "shell.execute_reply": "2026-04-19T10:04:37.709405Z" - } - }, + "id": "20", + "metadata": {}, "outputs": [], "source": [ "# DETACH DELETE atomically removes all edges then the node in one operation.\n", @@ -493,7 +423,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 caller" + "client.close() # injected client \u2014 owned by caller" ] } ], @@ -504,16 +434,7 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" + "name": "python" } }, "nbformat": 4, diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index cf1d213..8854414 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000001", + "id": "0", "metadata": {}, "source": [ "# LangGraph + CoordiNode: Agent with graph memory\n", @@ -10,22 +10,22 @@ "[![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", + "- `save_fact` \u2014 store a subject \u2192 relation \u2192 object triple in the graph\n", + "- `query_facts` \u2014 run an arbitrary Cypher query against the graph\n", + "- `find_related` \u2014 traverse the graph from a given entity\n", + "- `list_all_facts` \u2014 dump every fact in the current session\n", "\n", - "**Works without OpenAI** — the mock demo section calls tools directly. \n", + "**Works without OpenAI** \u2014 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." + "- **Google Colab** \u2014 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** \u2014 connects to a running CoordiNode server via gRPC." ] }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000002", + "id": "1", "metadata": {}, "source": [ "## Install dependencies" @@ -34,15 +34,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000003", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:40.114484Z", - "iopub.status.busy": "2026-04-19T10:04:40.114414Z", - "iopub.status.idle": "2026-04-19T10:04:41.329879Z", - "shell.execute_reply": "2026-04-19T10:04:41.329214Z" - } - }, + "id": "2", + "metadata": {}, "outputs": [], "source": [ "import os, sys, subprocess\n", @@ -56,9 +49,9 @@ "# 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", + " # Colab's apt packages ship rustc \u22641.75, which cannot build coordinode-embedded\n", + " # (requires Rust \u22651.80 for maturin/pyo3). apt-get is not a viable alternative here.\n", + " # Download the installer to a temp file and execute it explicitly \u2014 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", @@ -66,11 +59,11 @@ " # 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", + " # Skip embedded build if COORDINODE_ADDR is set \u2014 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", " # Security note: downloading rustup-init via HTTPS with cert verification and\n", - " # executing from a temp file (not piped to shell) is by design — this is the\n", + " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", @@ -125,7 +118,7 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000004", + "id": "3", "metadata": {}, "source": [ "## Connect to CoordiNode\n", @@ -137,15 +130,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000005", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.331594Z", - "iopub.status.busy": "2026-04-19T10:04:41.331499Z", - "iopub.status.idle": "2026-04-19T10:04:41.374936Z", - "shell.execute_reply": "2026-04-19T10:04:41.374260Z" - } - }, + "id": "4", + "metadata": {}, "outputs": [], "source": [ "import os, socket\n", @@ -162,7 +148,7 @@ "_use_embedded = True\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " # Explicit address — fail hard if health check fails.\n", + " # Explicit address \u2014 fail hard if health check fails.\n", " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", " from coordinode import CoordinodeClient\n", "\n", @@ -187,11 +173,11 @@ " print(f\"Connected to {COORDINODE_ADDR}\")\n", " _use_embedded = False\n", " else:\n", - " # Port is open but not a CoordiNode server — fall through to embedded.\n", + " # Port is open but not a CoordiNode server \u2014 fall through to embedded.\n", " client.close()\n", "\n", "if _use_embedded:\n", - " # No server available — use the embedded in-process engine.\n", + " # No server available \u2014 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", @@ -206,7 +192,7 @@ " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", " f\"Run: pip install {_pip_spec}\"\n", - " \" — or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " client = LocalClient(\":memory:\")\n", @@ -215,7 +201,7 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000006", + "id": "5", "metadata": {}, "source": [ "## 1. Define LangChain tools\n", @@ -227,15 +213,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000007", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.376845Z", - "iopub.status.busy": "2026-04-19T10:04:41.376702Z", - "iopub.status.idle": "2026-04-19T10:04:41.600914Z", - "shell.execute_reply": "2026-04-19T10:04:41.600502Z" - } - }, + "id": "6", + "metadata": {}, "outputs": [], "source": [ "import os, re, uuid\n", @@ -266,7 +245,7 @@ "\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", + " \"\"\"Save a fact (subject \u2192 relation \u2192 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", @@ -299,7 +278,7 @@ " _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", - " # Reject queries with parameters other than $sess — only {\"sess\": SESSION} is passed.\n", + " # Reject queries with parameters other than $sess \u2014 only {\"sess\": SESSION} is passed.\n", " extra_params = sorted(\n", " {m.group(1) for m in re.finditer(r\"\\$([A-Za-z_][A-Za-z0-9_]*)\", q)} - {\"sess\"}\n", " )\n", @@ -323,7 +302,7 @@ " 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", + " # is not yet supported by CoordiNode \u2014 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", @@ -356,10 +335,10 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000008", + "id": "7", "metadata": {}, "source": [ - "## 2. Mock demo — no LLM required (direct tool calls)\n", + "## 2. Mock demo \u2014 no LLM required (direct tool calls)\n", "\n", "Shows the full graph memory workflow by calling the tools directly." ] @@ -367,15 +346,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000009", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.602365Z", - "iopub.status.busy": "2026-04-19T10:04:41.602280Z", - "iopub.status.idle": "2026-04-19T10:04:41.628745Z", - "shell.execute_reply": "2026-04-19T10:04:41.628283Z" - } - }, + "id": "8", + "metadata": {}, "outputs": [], "source": [ "ACME_CORP = \"Acme Corp\" # constant used in several save_fact calls below\n", @@ -409,10 +381,10 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000010", + "id": "9", "metadata": {}, "source": [ - "## 3. LangGraph StateGraph — manual workflow\n", + "## 3. LangGraph StateGraph \u2014 manual workflow\n", "\n", "Shows how to wire CoordiNode tool calls into a LangGraph state machine without an LLM." ] @@ -420,15 +392,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000011", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.630239Z", - "iopub.status.busy": "2026-04-19T10:04:41.630164Z", - "iopub.status.idle": "2026-04-19T10:04:41.715197Z", - "shell.execute_reply": "2026-04-19T10:04:41.714712Z" - } - }, + "id": "10", + "metadata": {}, "outputs": [], "source": [ "from langgraph.graph import StateGraph, END\n", @@ -475,10 +440,10 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000012", + "id": "11", "metadata": {}, "source": [ - "## 4. LangGraph ReAct agent (optional — requires OPENAI_API_KEY)\n", + "## 4. LangGraph ReAct agent (optional \u2014 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", @@ -488,19 +453,12 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000013", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.716950Z", - "iopub.status.busy": "2026-04-19T10:04:41.716828Z", - "iopub.status.idle": "2026-04-19T10:04:41.719780Z", - "shell.execute_reply": "2026-04-19T10:04:41.719473Z" - } - }, + "id": "12", + "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", + " print(\"OPENAI_API_KEY not set \u2014 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", @@ -525,7 +483,7 @@ }, { "cell_type": "markdown", - "id": "d4e5f6a7-0003-0000-0000-000000000014", + "id": "13", "metadata": {}, "source": [ "## Cleanup" @@ -534,15 +492,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d4e5f6a7-0003-0000-0000-000000000015", - "metadata": { - "execution": { - "iopub.execute_input": "2026-04-19T10:04:41.721097Z", - "iopub.status.busy": "2026-04-19T10:04:41.720992Z", - "iopub.status.idle": "2026-04-19T10:04:41.731850Z", - "shell.execute_reply": "2026-04-19T10:04:41.731455Z" - } - }, + "id": "14", + "metadata": {}, "outputs": [], "source": [ "client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n", @@ -558,16 +509,7 @@ "name": "python3" }, "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" + "name": "python" } }, "nbformat": 4, From 8d69841d158674fe0d5db53d2498399879dd9f9e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 15:08:03 +0300 Subject: [PATCH 04/25] feat(sdk): expose consistency controls and document v0.4 features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cypher() now accepts read_concern / write_concern / read_preference / after_index — maps to ExecuteCypherRequest.ReadConcern, WriteConcern, ReadPreference proto fields. String-based API (e.g. "majority", "linearizable") validated against enum maps - Add _make_read_concern / _make_write_concern / _make_read_preference helpers and unit tests - Bump Colab embedded pin 8da94d6 -> c2ce320 (v0.3.17 -> v0.4.1) in all 4 demo notebooks - Update demo/README.md v0.3.17 -> v0.4.1 references - coordinode/README.md: drop "HNSW coming in v0.4" note, add Hybrid Search (rrf_score/text_score/vec_score), ATTACH/DETACH DOCUMENT, and Consistency Controls sections All 75 unit + 69 integration tests green; 4 demo notebooks re-executed end-to-end against v0.4.1 server. --- coordinode/README.md | 53 +++++++++- coordinode/coordinode/client.py | 97 ++++++++++++++++++- demo/README.md | 4 +- demo/notebooks/00_seed_data.ipynb | 4 +- .../01_llama_index_property_graph.ipynb | 4 +- demo/notebooks/02_langchain_graph_chain.ipynb | 6 +- demo/notebooks/03_langgraph_agent.ipynb | 4 +- tests/unit/test_consistency_helpers.py | 65 +++++++++++++ 8 files changed, 222 insertions(+), 15 deletions(-) create mode 100644 tests/unit/test_consistency_helpers.py diff --git a/coordinode/README.md b/coordinode/README.md index b2ad25f..c0e3d2b 100644 --- a/coordinode/README.md +++ b/coordinode/README.md @@ -105,7 +105,7 @@ db.cypher( params={"title": "RAG intro", "vec": [0.1] * 384}, ) -# Nearest-neighbour search (requires HNSW index — coming in v0.4) +# Nearest-neighbour search results = db.vector_search( label="Doc", property="embedding", @@ -117,6 +117,57 @@ for r in results: print(r.node.id, r.distance) ``` +## Hybrid Search (v0.4+) + +Fuse BM25 full-text and vector similarity using Cypher scoring functions: + +```python +# Reciprocal Rank Fusion of text + vector +rows = db.cypher(""" + MATCH (d:Doc) + WHERE text_match(d, $q) OR d.embedding IS NOT NULL + RETURN d, + rrf_score( + text_score(d, $q), + vec_score(d.embedding, $vec) + ) AS score + ORDER BY score DESC LIMIT 10 +""", params={"q": "graph neural network", "vec": [0.1] * 384}) +``` + +Helpers available in Cypher: ``text_score``, ``vec_score``, ``doc_score``, +``text_match``, ``rrf_score``, ``hybrid_score``. + +## ATTACH / DETACH DOCUMENT (v0.4+) + +Promote a nested property to a graph node (and back): + +```python +db.cypher("MATCH (a:Article {id: $id}) DETACH DOCUMENT a.body AS (d:Body)", + params={"id": 1}) +db.cypher("MATCH (a:Article {id: $id})-[:HAS_BODY]->(d:Body) " + "ATTACH DOCUMENT d INTO a.body", params={"id": 1}) +``` + +## Consistency Controls + +```python +# Majority read for strict freshness +db.cypher("MATCH (n:Account) RETURN n", read_concern="majority") + +# Majority write (required for causal reads) +db.cypher("CREATE (n:Event {t: timestamp()})", write_concern="majority") + +# Causal read: see at least state at raft index 42 +db.cypher("MATCH (n) RETURN count(n)", after_index=42) +``` + +Accepted values: + +- ``read_concern``: ``local`` (default) · ``majority`` · ``linearizable`` · ``snapshot`` +- ``write_concern``: ``w0`` · ``w1`` (default) · ``majority`` +- ``read_preference``: ``primary`` (default) · ``primary_preferred`` · ``secondary`` · ``secondary_preferred`` · ``nearest`` + ## Related Packages | Package | Description | diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index a20f9b8..907beda 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -253,8 +253,24 @@ async def cypher( self, query: str, params: dict[str, PyValue] | None = None, + *, + read_concern: str | None = None, + write_concern: str | None = None, + read_preference: str | None = None, + after_index: int | None = None, ) -> list[dict[str, Any]]: - """Execute an OpenCypher query. Returns rows as list of dicts.""" + """Execute an OpenCypher query. Returns rows as list of dicts. + + Consistency parameters (all optional; server defaults apply when omitted): + + - ``read_concern``: ``"local"`` (default), ``"majority"``, ``"linearizable"``, ``"snapshot"``. + - ``write_concern``: ``"w0"``, ``"w1"`` (default, leader-ack), ``"majority"``. Required + ``"majority"`` when using causal reads (``after_index`` > 0). + - ``read_preference``: ``"primary"`` (default), ``"primary_preferred"``, ``"secondary"``, + ``"secondary_preferred"``, ``"nearest"``. + - ``after_index``: raft log index for causal reads — returned rows reflect at least + the state at this index. + """ from coordinode._proto.coordinode.v1.query.cypher_pb2 import ( # type: ignore[import] ExecuteCypherRequest, ) @@ -263,6 +279,12 @@ async def cypher( query=query, parameters=dict_to_props(params or {}), ) + if read_concern is not None or after_index is not None: + req.read_concern.CopyFrom(_make_read_concern(read_concern, after_index)) + if write_concern is not None: + req.write_concern.CopyFrom(_make_write_concern(write_concern)) + if read_preference is not None: + req.read_preference = _make_read_preference(read_preference) resp = await self._cypher_stub.ExecuteCypher(req, timeout=self._timeout) columns = list(resp.columns) return [{col: from_property_value(val) for col, val in zip(columns, row.values)} for row in resp.rows] @@ -832,9 +854,23 @@ def cypher( self, query: str, params: dict[str, PyValue] | None = None, + *, + read_concern: str | None = None, + write_concern: str | None = None, + read_preference: str | None = None, + after_index: int | None = None, ) -> list[dict[str, Any]]: - """Execute an OpenCypher query. Returns rows as list of dicts.""" - return self._run(self._async.cypher(query, params)) + """Execute an OpenCypher query. See :meth:`AsyncCoordinodeClient.cypher` for consistency args.""" + return self._run( + self._async.cypher( + query, + params, + read_concern=read_concern, + write_concern=write_concern, + read_preference=read_preference, + after_index=after_index, + ) + ) def vector_search( self, @@ -945,6 +981,61 @@ def health(self) -> bool: return self._run(self._async.health()) +# ── Consistency helpers ────────────────────────────────────────────────────── + + +_READ_CONCERN_MAP = { + "local": "READ_CONCERN_LEVEL_LOCAL", + "majority": "READ_CONCERN_LEVEL_MAJORITY", + "linearizable": "READ_CONCERN_LEVEL_LINEARIZABLE", + "snapshot": "READ_CONCERN_LEVEL_SNAPSHOT", +} +_WRITE_CONCERN_MAP = { + "w0": "WRITE_CONCERN_LEVEL_W0", + "w1": "WRITE_CONCERN_LEVEL_W1", + "majority": "WRITE_CONCERN_LEVEL_MAJORITY", +} +_READ_PREFERENCE_MAP = { + "primary": "READ_PREFERENCE_PRIMARY", + "primary_preferred": "READ_PREFERENCE_PRIMARY_PREFERRED", + "secondary": "READ_PREFERENCE_SECONDARY", + "secondary_preferred": "READ_PREFERENCE_SECONDARY_PREFERRED", + "nearest": "READ_PREFERENCE_NEAREST", +} + + +def _make_read_concern(level: str | None, after_index: int | None) -> Any: + from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] + + enum_name = _READ_CONCERN_MAP.get(level.lower()) if level else None + if level and enum_name is None: + raise ValueError(f"invalid read_concern {level!r}; expected one of {sorted(_READ_CONCERN_MAP)}") + kwargs: dict[str, Any] = {} + if enum_name is not None: + kwargs["level"] = getattr(pb, enum_name) + if after_index is not None: + kwargs["after_index"] = int(after_index) + return pb.ReadConcern(**kwargs) + + +def _make_write_concern(level: str) -> Any: + from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] + + enum_name = _WRITE_CONCERN_MAP.get(level.lower()) + if enum_name is None: + raise ValueError(f"invalid write_concern {level!r}; expected one of {sorted(_WRITE_CONCERN_MAP)}") + return pb.WriteConcern(level=getattr(pb, enum_name)) + + +def _make_read_preference(pref: str) -> Any: + from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] + + enum_name = _READ_PREFERENCE_MAP.get(pref.lower()) + if enum_name is None: + raise ValueError(f"invalid read_preference {pref!r}; expected one of {sorted(_READ_PREFERENCE_MAP)}") + return getattr(pb, enum_name) + + # ── Stub factories (deferred import) ───────────────────────────────────────── diff --git a/demo/README.md b/demo/README.md index 974cfd7..c6992b9 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. -> The embedded Colab install is pinned to a specific commit that bundles coordinode-rs v0.3.17; the Colab notebook links above target `main`. -> The Docker Compose stack below uses the CoordiNode **server** image v0.3.17. +> The embedded Colab install is pinned to a specific commit that bundles coordinode-rs v0.4.1; the Colab notebook links above target `main`. +> The Docker Compose stack below uses the CoordiNode **server** image v0.4.1. ## Run locally (Docker Compose) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 36fe1a6..a5affff 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -98,7 +98,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#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@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", " \" \u2014 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 c005c57..98d3553 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -86,7 +86,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -222,7 +222,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@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", " \" \u2014 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 97daaed..5a2bf48 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -83,7 +83,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -105,7 +105,7 @@ " \"-q\",\n", " \"coordinode\",\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", @@ -221,7 +221,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@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", + " \"Run: pip install git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", " \" \u2014 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 8854414..02f86e5 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -43,7 +43,7 @@ "IN_COLAB = \"google.colab\" in sys.modules\n", "_EMBEDDED_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\"\n", + " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", @@ -187,7 +187,7 @@ " _pip_spec = globals().get(\n", " \"_EMBEDDED_PIP_SPEC\",\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@8da94d694ecaabee6f8380147d02f08220061bfa#subdirectory=coordinode-embedded\",\n", + " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", " )\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", diff --git a/tests/unit/test_consistency_helpers.py b/tests/unit/test_consistency_helpers.py new file mode 100644 index 0000000..5f8c91a --- /dev/null +++ b/tests/unit/test_consistency_helpers.py @@ -0,0 +1,65 @@ +"""Unit tests for consistency-parameter helpers in coordinode.client.""" + +from __future__ import annotations + +import pytest + +from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb +from coordinode.client import _make_read_concern, _make_read_preference, _make_write_concern + + +class TestReadConcern: + def test_level_only(self) -> None: + rc = _make_read_concern("majority", None) + assert rc.level == pb.READ_CONCERN_LEVEL_MAJORITY + assert rc.after_index == 0 + + def test_after_index_only(self) -> None: + rc = _make_read_concern(None, 42) + assert rc.after_index == 42 + + def test_level_and_after_index(self) -> None: + rc = _make_read_concern("linearizable", 7) + assert rc.level == pb.READ_CONCERN_LEVEL_LINEARIZABLE + assert rc.after_index == 7 + + def test_case_insensitive(self) -> None: + assert _make_read_concern("MAJORITY", None).level == pb.READ_CONCERN_LEVEL_MAJORITY + + def test_invalid_level_raises(self) -> None: + with pytest.raises(ValueError, match="invalid read_concern"): + _make_read_concern("strong", None) + + +class TestWriteConcern: + @pytest.mark.parametrize( + ("level", "expected"), + [ + ("w0", pb.WRITE_CONCERN_LEVEL_W0), + ("w1", pb.WRITE_CONCERN_LEVEL_W1), + ("majority", pb.WRITE_CONCERN_LEVEL_MAJORITY), + ], + ) + def test_valid_levels(self, level: str, expected: int) -> None: + assert _make_write_concern(level).level == expected + + def test_invalid_raises(self) -> None: + with pytest.raises(ValueError, match="invalid write_concern"): + _make_write_concern("w9") + + +class TestReadPreference: + @pytest.mark.parametrize( + ("pref", "expected"), + [ + ("primary", pb.READ_PREFERENCE_PRIMARY), + ("secondary_preferred", pb.READ_PREFERENCE_SECONDARY_PREFERRED), + ("nearest", pb.READ_PREFERENCE_NEAREST), + ], + ) + def test_valid(self, pref: str, expected: int) -> None: + assert _make_read_preference(pref) == expected + + def test_invalid_raises(self) -> None: + with pytest.raises(ValueError, match="invalid read_preference"): + _make_read_preference("leader") From 354a32dde28dc52c27c56b4c76dc3fd46f39210c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 15:32:32 +0300 Subject: [PATCH 05/25] fix(demo): install protobuf-compiler before embedded build in Colab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit coordinode-raft uses prost-build which requires protoc at build time. Colab runtime doesn't always have protobuf-compiler preinstalled, so the existing install block failed with: Error: Could not find `protoc`. Add an apt-get step inside the IN_COLAB branch before the rustup/maturin invocation. Verified end-to-end in an Ubuntu 22.04 Docker imitation of the Colab runtime — all 4 notebooks execute successfully through the embedded path. --- demo/notebooks/00_seed_data.ipynb | 1 + demo/notebooks/01_llama_index_property_graph.ipynb | 1 + demo/notebooks/02_langchain_graph_chain.ipynb | 1 + demo/notebooks/03_langgraph_agent.ipynb | 1 + 4 files changed, 4 insertions(+) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index a5affff..a6a318c 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -76,6 +76,7 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 98d3553..c30d30c 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -64,6 +64,7 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 5a2bf48..32a7e5b 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -61,6 +61,7 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 02f86e5..4db5076 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -65,6 +65,7 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", From 4175e967a5d935d54ab98b7b7f9035594e0f9e63 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 15:35:48 +0300 Subject: [PATCH 06/25] fix(sdk,demo): tighten consistency validation and pin coordinode SDK in Colab - Consistency helpers now share a _normalize_consistency_key guard that rejects None, non-str, blank/whitespace keys. after_index rejects bool, non-int, and negative values. Previously an empty string silently downgraded to defaults, True was coerced to 1, and non-string inputs crashed with AttributeError instead of ValueError. - Add unit tests covering blank strings, bool/negative/non-int after_index, and numeric inputs for all three helpers. - demo notebooks: pin the top-level coordinode SDK to the same git ref as coordinode-embedded inside the IN_COLAB branch (previously bare "coordinode" from PyPI could resolve to an incompatible newer release). - .gitignore: exclude .claude/ (session lock artefacts). --- .gitignore | 1 + coordinode/coordinode/client.py | 30 ++++++++++--------- demo/notebooks/00_seed_data.ipynb | 16 +++++++++- .../01_llama_index_property_graph.ipynb | 16 +++++++++- demo/notebooks/02_langchain_graph_chain.ipynb | 16 +++++++++- demo/notebooks/03_langgraph_agent.ipynb | 3 +- tests/unit/test_consistency_helpers.py | 20 +++++++++++++ 7 files changed, 84 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 4241e28..0727150 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ CLAUDE.md DEVLOG*.md **/.ipynb_checkpoints/ +.claude/ diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 907beda..d6b0db5 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -1004,36 +1004,38 @@ def health(self) -> bool: } +def _normalize_consistency_key(value: Any, field: str, mapping: dict[str, str]) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string; got {value!r}") + enum_name = mapping.get(value.strip().lower()) + if enum_name is None: + raise ValueError(f"invalid {field} {value!r}; expected one of {sorted(mapping)}") + return enum_name + + def _make_read_concern(level: str | None, after_index: int | None) -> Any: from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] - enum_name = _READ_CONCERN_MAP.get(level.lower()) if level else None - if level and enum_name is None: - raise ValueError(f"invalid read_concern {level!r}; expected one of {sorted(_READ_CONCERN_MAP)}") kwargs: dict[str, Any] = {} - if enum_name is not None: - kwargs["level"] = getattr(pb, enum_name) + if level is not None: + kwargs["level"] = getattr(pb, _normalize_consistency_key(level, "read_concern", _READ_CONCERN_MAP)) if after_index is not None: - kwargs["after_index"] = int(after_index) + if not isinstance(after_index, int) or isinstance(after_index, bool) or after_index < 0: + raise ValueError(f"after_index must be a non-negative integer, got {after_index!r}") + kwargs["after_index"] = after_index return pb.ReadConcern(**kwargs) def _make_write_concern(level: str) -> Any: from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] - enum_name = _WRITE_CONCERN_MAP.get(level.lower()) - if enum_name is None: - raise ValueError(f"invalid write_concern {level!r}; expected one of {sorted(_WRITE_CONCERN_MAP)}") - return pb.WriteConcern(level=getattr(pb, enum_name)) + return pb.WriteConcern(level=getattr(pb, _normalize_consistency_key(level, "write_concern", _WRITE_CONCERN_MAP))) def _make_read_preference(pref: str) -> Any: from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb # type: ignore[import] - enum_name = _READ_PREFERENCE_MAP.get(pref.lower()) - if enum_name is None: - raise ValueError(f"invalid read_preference {pref!r}; expected one of {sorted(_READ_PREFERENCE_MAP)}") - return getattr(pb, enum_name) + return getattr(pb, _normalize_consistency_key(pref, "read_preference", _READ_PREFERENCE_MAP)) # ── Stub factories (deferred import) ───────────────────────────────────────── diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index a6a318c..280585d 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -76,7 +76,8 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", @@ -104,6 +105,19 @@ " check=True,\n", " timeout=600,\n", " )\n", + " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + " )\n", "\n", "subprocess.run(\n", " [\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index c30d30c..5a91a56 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -64,7 +64,8 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", @@ -92,6 +93,19 @@ " check=True,\n", " timeout=600,\n", " )\n", + " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\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", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 32a7e5b..6cd8905 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -61,7 +61,8 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", @@ -89,6 +90,19 @@ " check=True,\n", " timeout=600,\n", " )\n", + " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", + " ],\n", + " check=True,\n", + " timeout=300,\n", + " )\n", "\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", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 4db5076..ae00edf 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -65,7 +65,8 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/tests/unit/test_consistency_helpers.py b/tests/unit/test_consistency_helpers.py index 5f8c91a..f5fea15 100644 --- a/tests/unit/test_consistency_helpers.py +++ b/tests/unit/test_consistency_helpers.py @@ -30,6 +30,16 @@ def test_invalid_level_raises(self) -> None: with pytest.raises(ValueError, match="invalid read_concern"): _make_read_concern("strong", None) + @pytest.mark.parametrize("bad", ["", " ", 5, True]) + def test_rejects_blank_or_non_string_level(self, bad: object) -> None: + with pytest.raises(ValueError, match="read_concern must be a non-empty string"): + _make_read_concern(bad, None) # type: ignore[arg-type] + + @pytest.mark.parametrize("bad", [True, False, -1, 1.5, "7"]) + def test_rejects_bool_negative_non_int_after_index(self, bad: object) -> None: + with pytest.raises(ValueError, match="after_index must be a non-negative integer"): + _make_read_concern(None, bad) # type: ignore[arg-type] + class TestWriteConcern: @pytest.mark.parametrize( @@ -47,6 +57,11 @@ def test_invalid_raises(self) -> None: with pytest.raises(ValueError, match="invalid write_concern"): _make_write_concern("w9") + @pytest.mark.parametrize("bad", ["", " ", None, 1]) + def test_rejects_blank_or_non_string(self, bad: object) -> None: + with pytest.raises(ValueError, match="write_concern must be a non-empty string"): + _make_write_concern(bad) # type: ignore[arg-type] + class TestReadPreference: @pytest.mark.parametrize( @@ -63,3 +78,8 @@ def test_valid(self, pref: str, expected: int) -> None: def test_invalid_raises(self) -> None: with pytest.raises(ValueError, match="invalid read_preference"): _make_read_preference("leader") + + @pytest.mark.parametrize("bad", ["", " ", None, 0]) + def test_rejects_blank_or_non_string(self, bad: object) -> None: + with pytest.raises(ValueError, match="read_preference must be a non-empty string"): + _make_read_preference(bad) # type: ignore[arg-type] From 4f6a797ae72ae97fdb285aa4312a82b0bc34b15a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 15:46:23 +0300 Subject: [PATCH 07/25] fix(demo): pin coordinode SDK in 03 notebook Colab branch, surface unhealthy-port fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _SDK_PIP_SPEC alongside _EMBEDDED_PIP_SPEC; use the pinned spec in the Colab branch so the SDK and embedded package resolve to the same git commit. Outside Colab the unpinned "coordinode" still comes from PyPI (editable installs / released wheels). - Port-probe fallback path now prints an explicit WARNING before switching to LocalClient(":memory:") — previously the silent close() could surprise a user who expected the local server to be used. --- demo/notebooks/03_langgraph_agent.ipynb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index ae00edf..3758723 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -45,6 +45,10 @@ " \"git+https://github.com/structured-world/coordinode-python.git\"\n", " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", ")\n", + "_SDK_PIP_SPEC = (\n", + " \"git+https://github.com/structured-world/coordinode-python.git\"\n", + " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", "if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\"):\n", @@ -95,6 +99,7 @@ " timeout=600,\n", " )\n", "\n", + "_coordinode_spec = _SDK_PIP_SPEC if IN_COLAB else \"coordinode\"\n", "subprocess.run(\n", " [\n", " sys.executable,\n", @@ -102,7 +107,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"coordinode\",\n", + " _coordinode_spec,\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"langgraph\",\n", @@ -175,7 +180,8 @@ " print(f\"Connected to {COORDINODE_ADDR}\")\n", " _use_embedded = False\n", " else:\n", - " # Port is open but not a CoordiNode server \u2014 fall through to embedded.\n", + " # Port is open but server is unhealthy \u2014 surface clearly before falling back.\n", + " print(f\"WARNING: port {grpc_port} is open but health check failed \u2014 falling back to embedded LocalClient(':memory:'). Data will be in-process only.\")\n", " client.close()\n", "\n", "if _use_embedded:\n", From c8cea550da63e410a5e6e30f4ebafd9b512ef85a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 16:11:49 +0300 Subject: [PATCH 08/25] fix(demo): remove unpinned coordinode override from Colab install block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous fix installed coordinode from the git pin inside IN_COLAB, but the unconditional subprocess.run at the end of the cell still installed bare "coordinode" from PyPI — pip treated this as an upgrade and overrode the pinned version with whatever PyPI resolved to. Align 00/01/02 with 03's pattern: single _coordinode_spec variable that evaluates to the git-pin URL when IN_COLAB else bare "coordinode" for local dev. --- demo/notebooks/00_seed_data.ipynb | 20 ++++++------------- .../01_llama_index_property_graph.ipynb | 20 ++++++------------- demo/notebooks/02_langchain_graph_chain.ipynb | 20 ++++++------------- 3 files changed, 18 insertions(+), 42 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 280585d..35d414f 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -105,20 +105,12 @@ " check=True,\n", " timeout=600,\n", " )\n", - " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - " )\n", "\n", + "_coordinode_spec = (\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " if IN_COLAB\n", + " else \"coordinode\"\n", + ")\n", "subprocess.run(\n", " [\n", " sys.executable,\n", @@ -126,7 +118,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"coordinode\",\n", + " _coordinode_spec,\n", " \"nest_asyncio\",\n", " ],\n", " check=True,\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 5a91a56..3c83b55 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -93,24 +93,16 @@ " check=True,\n", " timeout=600,\n", " )\n", - " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\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", + "_coordinode_spec = (\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " if IN_COLAB\n", + " else \"coordinode\"\n", + ")\n", "subprocess.run(\n", " [\n", " sys.executable,\n", @@ -118,7 +110,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"coordinode\",\n", + " _coordinode_spec,\n", " \"llama-index-graph-stores-coordinode\",\n", " \"llama-index-core\",\n", " \"nest_asyncio\",\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 6cd8905..7c1c7ae 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -90,19 +90,6 @@ " check=True,\n", " timeout=600,\n", " )\n", - " # SDK (gRPC client helpers used by LocalClient wrapper) must come from the same pin.\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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\",\n", - " ],\n", - " check=True,\n", - " timeout=300,\n", - " )\n", "\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", @@ -111,6 +98,11 @@ "# 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", + "_coordinode_spec = (\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " if IN_COLAB\n", + " else \"coordinode\"\n", + ")\n", "subprocess.run(\n", " [\n", " sys.executable,\n", @@ -118,7 +110,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"coordinode\",\n", + " _coordinode_spec,\n", " \"langchain\",\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", From 5957b8bb43ce0ee5c47ac2ff5f773cc599566200 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 16:33:27 +0300 Subject: [PATCH 09/25] fix(demo): tighten pin condition and hard-fail on unhealthy gRPC port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _coordinode_spec now uses `IN_COLAB and not os.environ.get("COORDINODE_ADDR")` matching the embedded install gate. Previously, a Colab user with COORDINODE_ADDR pointed at their own server would still install the SDK from the git pin unnecessarily. - 03 port-probe: when port 7080 is open but health check fails, raise RuntimeError instead of silently falling through to LocalClient(":memory:"). A listening port is a strong signal the user expected a real server — masking the failure by writing to ephemeral in-memory storage is surprising and hides real problems. --- demo/notebooks/00_seed_data.ipynb | 2 +- .../01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 16 +++++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 35d414f..95f9075 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -108,7 +108,7 @@ "\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", - " if IN_COLAB\n", + " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 3c83b55..d6f7fc4 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -100,7 +100,7 @@ "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", - " if IN_COLAB\n", + " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 7c1c7ae..d99eba3 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -100,7 +100,7 @@ "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", - " if IN_COLAB\n", + " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 3758723..fe3af05 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -99,7 +99,7 @@ " timeout=600,\n", " )\n", "\n", - "_coordinode_spec = _SDK_PIP_SPEC if IN_COLAB else \"coordinode\"\n", + "_coordinode_spec = _SDK_PIP_SPEC if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\") else \"coordinode\"\n", "subprocess.run(\n", " [\n", " sys.executable,\n", @@ -176,13 +176,15 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " if client.health():\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " _use_embedded = False\n", - " else:\n", - " # Port is open but server is unhealthy \u2014 surface clearly before falling back.\n", - " print(f\"WARNING: port {grpc_port} is open but health check failed \u2014 falling back to embedded LocalClient(':memory:'). Data will be in-process only.\")\n", + " if not client.health():\n", " client.close()\n", + " raise RuntimeError(\n", + " f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks. \"\n", + " \"Set COORDINODE_ADDR to an explicit address, or stop the process \"\n", + " f\"holding port {grpc_port} to let the notebook use embedded storage.\"\n", + " )\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " _use_embedded = False\n", "\n", "if _use_embedded:\n", " # No server available \u2014 use the embedded in-process engine.\n", From 443b189e2ad7f28fb378517f10b896b9948b3a16 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 16:56:35 +0300 Subject: [PATCH 10/25] fix(demo,tests,docs): guard apt install, proto stub import, README examples, and port-probe fallback - Notebooks (4x): guard apt install with which-protoc check and run apt-get update first so long-lived Colab runtimes with stale indexes do not fail. - tests/unit/test_consistency_helpers.py: wrap proto stub import in pytest.importorskip so fresh checkouts without make proto still collect the suite. - coordinode/README.md hybrid + consistency examples: project explicit properties and alias the node handle as doc_id / node_id since bare RETURN d returns an integer node id. Mention get_node() for full-node lookups. - 03 notebook port-probe auto-detect: restore fallback to embedded when port 7080 is open but health check fails. Explicit COORDINODE_ADDR still hard-fails. --- coordinode/README.md | 17 ++++++++---- demo/notebooks/00_seed_data.ipynb | 7 +++-- .../01_llama_index_property_graph.ipynb | 7 +++-- demo/notebooks/02_langchain_graph_chain.ipynb | 7 +++-- demo/notebooks/03_langgraph_agent.ipynb | 27 ++++++++++++------- tests/unit/test_consistency_helpers.py | 11 ++++++-- 6 files changed, 53 insertions(+), 23 deletions(-) diff --git a/coordinode/README.md b/coordinode/README.md index c0e3d2b..ef546bf 100644 --- a/coordinode/README.md +++ b/coordinode/README.md @@ -122,17 +122,20 @@ for r in results: Fuse BM25 full-text and vector similarity using Cypher scoring functions: ```python -# Reciprocal Rank Fusion of text + vector +# Reciprocal Rank Fusion of text + vector. Projecting `d AS doc_id` returns the +# internal node id (an integer) — fetch properties explicitly when needed. rows = db.cypher(""" MATCH (d:Doc) WHERE text_match(d, $q) OR d.embedding IS NOT NULL - RETURN d, + RETURN d AS doc_id, + d.title AS title, rrf_score( text_score(d, $q), vec_score(d.embedding, $vec) ) AS score ORDER BY score DESC LIMIT 10 """, params={"q": "graph neural network", "vec": [0.1] * 384}) +# Full node properties: db.get_node(rows[0]["doc_id"]). ``` Helpers available in Cypher: ``text_score``, ``vec_score``, ``doc_score``, @@ -152,14 +155,18 @@ db.cypher("MATCH (a:Article {id: $id})-[:HAS_BODY]->(d:Body) " ## Consistency Controls ```python -# Majority read for strict freshness -db.cypher("MATCH (n:Account) RETURN n", read_concern="majority") +# Majority read for strict freshness. `n AS node_id` returns the integer id; +# use get_node(id) or project explicit properties (e.g. n.email AS email). +db.cypher( + "MATCH (n:Account) RETURN n AS node_id, n.email AS email", + read_concern="majority", +) # Majority write (required for causal reads) db.cypher("CREATE (n:Event {t: timestamp()})", write_concern="majority") # Causal read: see at least state at raft index 42 -db.cypher("MATCH (n) RETURN count(n)", after_index=42) +db.cypher("MATCH (n) RETURN count(n) AS total", after_index=42) ``` Accepted values: diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 95f9075..dff6fa2 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -76,8 +76,11 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", - " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Skip if already present (faster reruns),\n", + " # otherwise refresh apt indexes first \u2014 Colab caches can go stale on long-lived runtimes.\n", + " if subprocess.run([\"which\", \"protoc\"], capture_output=True).returncode != 0:\n", + " subprocess.run([\"apt-get\", \"update\", \"-y\", \"-q\"], check=True, timeout=120)\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index d6f7fc4..6208820 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -64,8 +64,11 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", - " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Skip if already present (faster reruns),\n", + " # otherwise refresh apt indexes first \u2014 Colab caches can go stale on long-lived runtimes.\n", + " if subprocess.run([\"which\", \"protoc\"], capture_output=True).returncode != 0:\n", + " subprocess.run([\"apt-get\", \"update\", \"-y\", \"-q\"], check=True, timeout=120)\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index d99eba3..c77bce3 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -61,8 +61,11 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", - " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Skip if already present (faster reruns),\n", + " # otherwise refresh apt indexes first \u2014 Colab caches can go stale on long-lived runtimes.\n", + " if subprocess.run([\"which\", \"protoc\"], capture_output=True).returncode != 0:\n", + " subprocess.run([\"apt-get\", \"update\", \"-y\", \"-q\"], check=True, timeout=120)\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index fe3af05..dc1c3b5 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -69,8 +69,11 @@ " # Security note: downloading rustup-init via HTTPS with cert verification and\n", " # executing from a temp file (not piped to shell) is by design \u2014 this is the\n", " # rustup project's own recommended install method for automated environments.\n", - " # protoc is required by coordinode-raft build (prost-build). Colab allows apt without sudo prompts.\n", - " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", + " # protoc is required by coordinode-raft build (prost-build). Skip if already present (faster reruns),\n", + " # otherwise refresh apt indexes first \u2014 Colab caches can go stale on long-lived runtimes.\n", + " if subprocess.run([\"which\", \"protoc\"], capture_output=True).returncode != 0:\n", + " subprocess.run([\"apt-get\", \"update\", \"-y\", \"-q\"], check=True, timeout=120)\n", + " subprocess.run([\"apt-get\", \"install\", \"-y\", \"-q\", \"protobuf-compiler\"], check=True, timeout=120)\n", " import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n", "\n", " _ctx = _ssl.create_default_context()\n", @@ -176,15 +179,19 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(\n", - " f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks. \"\n", - " \"Set COORDINODE_ADDR to an explicit address, or stop the process \"\n", - " f\"holding port {grpc_port} to let the notebook use embedded storage.\"\n", + " if client.health():\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " _use_embedded = False\n", + " else:\n", + " # Auto-detect path only: port is open but health failed \u2014 likely an\n", + " # unrelated process on 7080. Fall back to embedded (user did not\n", + " # explicitly request this server). Explicit COORDINODE_ADDR above\n", + " # still hard-fails, which is where strict behaviour matters.\n", + " print(\n", + " f\"Port {grpc_port} is open but health check failed \u2014 not a CoordiNode server. \"\n", + " \"Falling back to embedded LocalClient(':memory:').\"\n", " )\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " _use_embedded = False\n", + " client.close()\n", "\n", "if _use_embedded:\n", " # No server available \u2014 use the embedded in-process engine.\n", diff --git a/tests/unit/test_consistency_helpers.py b/tests/unit/test_consistency_helpers.py index f5fea15..c987fc0 100644 --- a/tests/unit/test_consistency_helpers.py +++ b/tests/unit/test_consistency_helpers.py @@ -4,8 +4,15 @@ import pytest -from coordinode._proto.coordinode.v1.replication import consistency_pb2 as pb -from coordinode.client import _make_read_concern, _make_read_preference, _make_write_concern +# Proto stubs are generated by `make proto` and gitignored; skip the whole module +# when running on a fresh checkout that has not regenerated them yet. +pb = pytest.importorskip("coordinode._proto.coordinode.v1.replication.consistency_pb2") + +from coordinode.client import ( # noqa: E402 — guarded import above + _make_read_concern, + _make_read_preference, + _make_write_concern, +) class TestReadConcern: From 991f6e760b3180e02fc12ba374196ad34d7fe2d3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 17:55:00 +0300 Subject: [PATCH 11/25] fix(demo): close failed CoordinodeClient before raise; reinstate hard-fail on unhealthy port 7080 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 00 and 02: close() the client in the health-check failure branches before raising, so the gRPC channel does not leak across notebook kernel reruns. - 03 port-probe: replace silent fallback to LocalClient(":memory:") with a hard RuntimeError when port 7080 is open but health fails. Colab runtimes never have third parties on 7080, so this branch only triggers locally where the user launched CoordiNode themselves — silent fallback would hide the real problem and mask data loss. Code comment documents the reasoning for future reviewers. --- demo/notebooks/00_seed_data.ipynb | 2 ++ demo/notebooks/02_langchain_graph_chain.ipynb | 2 ++ demo/notebooks/03_langgraph_agent.ipynb | 24 +++++++++---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index dff6fa2..879b327 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -171,6 +171,7 @@ "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", " if not client.health():\n", + " client.close()\n", " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", @@ -185,6 +186,7 @@ "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", " if not client.health():\n", + " client.close()\n", " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index c77bce3..16504a9 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -207,6 +207,7 @@ "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", " if not client.health():\n", + " client.close()\n", " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", @@ -221,6 +222,7 @@ "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", " if not client.health():\n", + " client.close()\n", " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", " else:\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index dc1c3b5..18f4b83 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -179,19 +179,19 @@ " from coordinode import CoordinodeClient\n", "\n", " client = CoordinodeClient(COORDINODE_ADDR)\n", - " if client.health():\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " _use_embedded = False\n", - " else:\n", - " # Auto-detect path only: port is open but health failed \u2014 likely an\n", - " # unrelated process on 7080. Fall back to embedded (user did not\n", - " # explicitly request this server). Explicit COORDINODE_ADDR above\n", - " # still hard-fails, which is where strict behaviour matters.\n", - " print(\n", - " f\"Port {grpc_port} is open but health check failed \u2014 not a CoordiNode server. \"\n", - " \"Falling back to embedded LocalClient(':memory:').\"\n", - " )\n", + " # Port open + health fail \u21d2 misconfigured or version-mismatched CoordiNode.\n", + " # In Colab runtimes port 7080 is never occupied by a third party (fresh VM,\n", + " # no long-lived services), so this branch only triggers locally where the\n", + " # user launched CoordiNode themselves \u2014 silent fallback to :memory: would\n", + " # hide the real problem and mask data loss. Hard-fail surfaces it.\n", + " if not client.health():\n", " client.close()\n", + " raise RuntimeError(\n", + " f\"Port {grpc_port} is open but CoordiNode health check failed for {COORDINODE_ADDR}. \"\n", + " f\"Fix the server, or set COORDINODE_PORT to an unused port to use embedded storage.\"\n", + " )\n", + " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " _use_embedded = False\n", "\n", "if _use_embedded:\n", " # No server available \u2014 use the embedded in-process engine.\n", From 0b7fd95245f48eab3ae9ddb3e9499171c68a89f4 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 18:08:17 +0300 Subject: [PATCH 12/25] fix(demo): drop port probe, switch embedded to file-backed persistence The port-probe auto-detect branch was unreachable in Colab (fresh VMs have nothing on port 7080) and caused review-loop churn locally. Drop it and use the simple two-way branch: explicit COORDINODE_ADDR or embedded. :memory: embedded storage was lost on every cell rerun and made the 00 to 03 notebook chain impossible in Colab without a server. Switch to a file-backed LocalClient with COORDINODE_EMBEDDED_PATH env var (default /content/coordinode-demo.db in Colab, /tmp/coordinode-demo.db locally). Graph now persists across reruns and is shared between sibling demo notebooks in the same runtime. --- demo/notebooks/00_seed_data.ipynb | 69 ++++++++----------- .../01_llama_index_property_graph.ipynb | 65 +++++++---------- demo/notebooks/02_langchain_graph_chain.ipynb | 63 +++++++---------- demo/notebooks/03_langgraph_agent.ipynb | 69 +++++-------------- 4 files changed, 93 insertions(+), 173 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 879b327..32b62cd 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -11,10 +11,12 @@ "\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 \u2014 notebooks 01\u201303 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", + "> **Note:** Embedded mode writes to `COORDINODE_EMBEDDED_PATH` (default\n", + "> `/content/coordinode-demo.db` in Colab, `/tmp/coordinode-demo.db` locally), so\n", + "> the seeded graph persists across cell reruns and is visible to sibling demo\n", + "> notebooks within the same runtime. Delete the file or set a different path to\n", + "> reset. Connecting to a real CoordiNode server via `COORDINODE_ADDR` is also\n", + "> supported.\n", "\n", "**Graph contents:**\n", "- 10 people (engineers, researchers, founders)\n", @@ -142,7 +144,7 @@ "source": [ "## Connect to CoordiNode\n", "\n", - "- **Colab**: uses `LocalClient(\":memory:\")` \u2014 in-process embedded engine, no server required.\n", + "- **Colab**: uses `LocalClient(COORDINODE_EMBEDDED_PATH)` \u2014 in-process embedded engine backed by a file under `/content/`, 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." ] @@ -154,16 +156,15 @@ "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", + "import os\n", + "\n", + "# Persistent embedded DB path. Colab has /content which persists across cell\n", + "# reruns within a runtime session; locally fall back to /tmp. Override via\n", + "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", + " \"COORDINODE_EMBEDDED_PATH\",\n", + " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\n", @@ -175,33 +176,19 @@ " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", "else:\n", + " # No explicit server \u2014 use the embedded in-process engine backed by a file\n", + " # so the graph persists across cell reruns and between sibling demo\n", + " # notebooks within the same runtime.\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", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " else:\n", - " # No server available \u2014 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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", - " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", - " ) from exc\n", - "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")" + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " ) from exc\n", + "\n", + " client = LocalClient(COORDINODE_EMBEDDED_PATH)\n", + " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 6208820..2cdef95 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -189,56 +189,41 @@ "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", + "import os\n", "\n", + "# Persistent embedded DB path. Colab has /content which persists across cell\n", + "# reruns within a runtime session; locally fall back to /tmp. Override via\n", + "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", + " \"COORDINODE_EMBEDDED_PATH\",\n", + " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\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", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(f\"CoordiNode at {COORDINODE_ADDR} is not serving health checks\")\n", + " _cc = CoordinodeClient(COORDINODE_ADDR)\n", + " if not _cc.health():\n", + " _cc.close()\n", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " client = _cc\n", "else:\n", + " # No explicit server \u2014 use the embedded in-process engine backed by a file\n", + " # so the graph persists across cell reruns and between sibling demo\n", + " # notebooks within the same runtime.\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", - " 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 \u2014 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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", - " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", - " ) from exc\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run the install cell above, 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)\")\n" + " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", + " client = _lc\n", + " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, { diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 16504a9..1c8d73f 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -190,56 +190,41 @@ "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", + "import os\n", "\n", + "# Persistent embedded DB path. Colab has /content which persists across cell\n", + "# reruns within a runtime session; locally fall back to /tmp. Override via\n", + "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", + " \"COORDINODE_EMBEDDED_PATH\",\n", + " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\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", - " if not client.health():\n", - " client.close()\n", + " _cc = CoordinodeClient(COORDINODE_ADDR)\n", + " if not _cc.health():\n", + " _cc.close()\n", " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", + " client = _cc\n", "else:\n", + " # No explicit server \u2014 use the embedded in-process engine backed by a file\n", + " # so the graph persists across cell reruns and between sibling demo\n", + " # notebooks within the same runtime.\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", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " else:\n", - " # No server available \u2014 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@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", - " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", - " ) from exc\n", + " from coordinode_embedded import LocalClient\n", + " except ImportError as exc:\n", + " raise RuntimeError(\n", + " \"coordinode-embedded is not installed. \"\n", + " \"Run the install cell above, 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)\")" + " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", + " client = _lc\n", + " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, { diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 18f4b83..45b1bcc 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -144,76 +144,39 @@ "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", - "_use_embedded = True\n", + "import os\n", + "\n", + "# Persistent embedded DB path. Colab has /content which persists across cell\n", + "# reruns within a runtime session; locally fall back to /tmp. Override via\n", + "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", + " \"COORDINODE_EMBEDDED_PATH\",\n", + " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", - " # Explicit address \u2014 fail hard if health check fails.\n", " COORDINODE_ADDR = os.environ[\"COORDINODE_ADDR\"]\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", + " raise RuntimeError(f\"Health check failed for {COORDINODE_ADDR}\")\n", " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " _use_embedded = False\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", - " # Port open + health fail \u21d2 misconfigured or version-mismatched CoordiNode.\n", - " # In Colab runtimes port 7080 is never occupied by a third party (fresh VM,\n", - " # no long-lived services), so this branch only triggers locally where the\n", - " # user launched CoordiNode themselves \u2014 silent fallback to :memory: would\n", - " # hide the real problem and mask data loss. Hard-fail surfaces it.\n", - " if not client.health():\n", - " client.close()\n", - " raise RuntimeError(\n", - " f\"Port {grpc_port} is open but CoordiNode health check failed for {COORDINODE_ADDR}. \"\n", - " f\"Fix the server, or set COORDINODE_PORT to an unused port to use embedded storage.\"\n", - " )\n", - " print(f\"Connected to {COORDINODE_ADDR}\")\n", - " _use_embedded = False\n", - "\n", - "if _use_embedded:\n", - " # No server available \u2014 use the embedded in-process engine.\n", - " # Works without Docker or any external service; data is in-memory.\n", + " # No explicit server \u2014 use the embedded in-process engine backed by a file\n", + " # so the graph persists across cell reruns and between sibling demo\n", + " # notebooks within the same runtime.\n", " try:\n", " from coordinode_embedded import LocalClient\n", " except ImportError as exc:\n", - " # _EMBEDDED_PIP_SPEC is defined in the install cell; fall back to the\n", - " # pinned spec so this cell remains runnable if executed standalone.\n", - " _pip_spec = globals().get(\n", - " \"_EMBEDDED_PIP_SPEC\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", - " )\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " f\"Run: pip install {_pip_spec}\"\n", - " \" \u2014 or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", - " client = LocalClient(\":memory:\")\n", - " print(\"Using embedded LocalClient (in-process)\")\n" + " client = LocalClient(COORDINODE_EMBEDDED_PATH)\n", + " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, { From d53ee73c31159312bbfe335175a3157501a3e1ed Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 18:11:31 +0300 Subject: [PATCH 13/25] test(integration): exercise cypher() consistency kwargs; build(demo): bump pin - Add test_cypher_accepts_consistency_kwargs: write with majority, read back with read_concern=majority + primary + after_index=0, asserts the value round-trips. - Add test_cypher_rejects_invalid_consistency_values: verifies ValueError for strong / w9 / leader / after_index=-1. - Bump Colab embedded + SDK pin to 0b7fd95 in 4 demo notebooks. --- demo/notebooks/00_seed_data.ipynb | 4 +- .../01_llama_index_property_graph.ipynb | 4 +- demo/notebooks/02_langchain_graph_chain.ipynb | 6 +-- demo/notebooks/03_langgraph_agent.ipynb | 4 +- tests/integration/test_sdk.py | 37 +++++++++++++++++++ 5 files changed, 46 insertions(+), 9 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 32b62cd..6d921df 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -105,14 +105,14 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", " )\n", "\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 2cdef95..7835b4e 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -91,7 +91,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# 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", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 1c8d73f..3e93c08 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -88,7 +88,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# The remaining packages (coordinode, LangChain, etc.) are installed without pins:\n", "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", " else \"coordinode\"\n", ")\n", @@ -115,7 +115,7 @@ " \"-q\",\n", " _coordinode_spec,\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 45b1bcc..6a77678 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -43,11 +43,11 @@ "IN_COLAB = \"google.colab\" in sys.modules\n", "_EMBEDDED_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode-embedded\"\n", + " \"@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\"\n", ")\n", "_SDK_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@c2ce32064dd7f6495f0998049c0cc2f6f7a5767d#subdirectory=coordinode\"\n", + " \"@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index b7e4fc8..27325ee 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -693,3 +693,40 @@ def test_text_search_fuzzy(client): client.drop_text_index(idx_name) finally: client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + + +def test_cypher_accepts_consistency_kwargs(client): + """cypher() wires read_concern / write_concern / read_preference / after_index into the request.""" + # Write with majority concern then read back with majority + primary + after_index. + label = f"ConsistencyTest_{uid()}" + tag = uid() + client.cypher( + f"CREATE (n:{label} {{tag: $tag, v: 1}})", + params={"tag": tag}, + write_concern="majority", + ) + rows = client.cypher( + f"MATCH (n:{label} {{tag: $tag}}) RETURN n.v AS v", + params={"tag": tag}, + read_concern="majority", + read_preference="primary", + after_index=0, + ) + try: + assert rows and rows[0]["v"] == 1 + finally: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + + +def test_cypher_rejects_invalid_consistency_values(client): + """Invalid consistency kwargs raise ValueError before the RPC.""" + import pytest as _pytest + + with _pytest.raises(ValueError, match="invalid read_concern"): + client.cypher("RETURN 1", read_concern="strong") + with _pytest.raises(ValueError, match="invalid write_concern"): + client.cypher("RETURN 1", write_concern="w9") + with _pytest.raises(ValueError, match="invalid read_preference"): + client.cypher("RETURN 1", read_preference="leader") + with _pytest.raises(ValueError, match="after_index must be a non-negative integer"): + client.cypher("RETURN 1", after_index=-1) From c8ae14952b16382ccefce7dbca30d6367014b03e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 18:30:07 +0300 Subject: [PATCH 14/25] style(tests,build): use module-level pytest import; gitignore embedded .so - tests/integration/test_sdk.py: drop redundant `import pytest as _pytest`, use the top-level pytest import. - coordinode-embedded/.gitignore: add `*.so` so locally built Rust extension modules don't get committed with platform suffix. --- coordinode-embedded/.gitignore | 1 + tests/integration/test_sdk.py | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/coordinode-embedded/.gitignore b/coordinode-embedded/.gitignore index 2c96eb1..d91254f 100644 --- a/coordinode-embedded/.gitignore +++ b/coordinode-embedded/.gitignore @@ -1,2 +1,3 @@ target/ Cargo.lock +*.so diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 27325ee..51ff79f 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -720,13 +720,11 @@ def test_cypher_accepts_consistency_kwargs(client): def test_cypher_rejects_invalid_consistency_values(client): """Invalid consistency kwargs raise ValueError before the RPC.""" - import pytest as _pytest - - with _pytest.raises(ValueError, match="invalid read_concern"): + with pytest.raises(ValueError, match="invalid read_concern"): client.cypher("RETURN 1", read_concern="strong") - with _pytest.raises(ValueError, match="invalid write_concern"): + with pytest.raises(ValueError, match="invalid write_concern"): client.cypher("RETURN 1", write_concern="w9") - with _pytest.raises(ValueError, match="invalid read_preference"): + with pytest.raises(ValueError, match="invalid read_preference"): client.cypher("RETURN 1", read_preference="leader") - with _pytest.raises(ValueError, match="after_index must be a non-negative integer"): + with pytest.raises(ValueError, match="after_index must be a non-negative integer"): client.cypher("RETURN 1", after_index=-1) From e938194bf9708cf6da2a4783dff5b9124e848a5f Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 18:55:46 +0300 Subject: [PATCH 15/25] fix(demo,tests,docs): restore embedded adapter wrap, tighten test cleanup, clarify Cypher helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 01/02 notebooks: wrap LocalClient in _EmbeddedAdapter so CoordinodeGraph / CoordinodePropertyGraphStore can resolve get_schema_text() in embedded mode. Without the adapter, refresh_schema() returned an empty string. - 00/03 markdown: drop references to the auto-probed localhost:7080 default; document the explicit COORDINODE_ADDR or COORDINODE_EMBEDDED_PATH split that matches the current connect cells. - tests/integration/test_sdk.py::test_cypher_accepts_consistency_kwargs: wrap CREATE / MATCH / assert in the try block with a `created` flag so cleanup runs only after a successful CREATE. - coordinode/README.md: clarify that text_score / vec_score / doc_score / text_match / rrf_score / hybrid_score are server-side Cypher functions (coordinode-rs ≥ v0.4.0), not Python-side helpers. --- coordinode/README.md | 6 ++-- demo/notebooks/00_seed_data.ipynb | 4 +-- .../01_llama_index_property_graph.ipynb | 4 ++- demo/notebooks/02_langchain_graph_chain.ipynb | 4 ++- demo/notebooks/03_langgraph_agent.ipynb | 2 +- tests/integration/test_sdk.py | 30 ++++++++++--------- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/coordinode/README.md b/coordinode/README.md index ef546bf..d9c49f0 100644 --- a/coordinode/README.md +++ b/coordinode/README.md @@ -138,8 +138,10 @@ rows = db.cypher(""" # Full node properties: db.get_node(rows[0]["doc_id"]). ``` -Helpers available in Cypher: ``text_score``, ``vec_score``, ``doc_score``, -``text_match``, ``rrf_score``, ``hybrid_score``. +Helpers available in Cypher (evaluated server-side in coordinode-rs ≥ v0.4.0): +``text_score``, ``vec_score``, ``doc_score``, ``text_match``, ``rrf_score``, +``hybrid_score``. These are built-in Cypher functions; nothing to import on the +Python side. ## ATTACH / DETACH DOCUMENT (v0.4+) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 6d921df..d0aeb3f 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -145,8 +145,8 @@ "## Connect to CoordiNode\n", "\n", "- **Colab**: uses `LocalClient(COORDINODE_EMBEDDED_PATH)` \u2014 in-process embedded engine backed by a file under `/content/`, 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." + "- **Local with server**: set `COORDINODE_ADDR=host:port` to point at a running CoordiNode (no auto-probe \u2014 explicit only).\n", + "- **Local without server**: uses `coordinode-embedded` (file-backed at `COORDINODE_EMBEDDED_PATH`, default `/tmp/coordinode-demo.db`). Raises `RuntimeError` with install instructions if the package is missing." ] }, { diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 7835b4e..4e3d063 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -222,7 +222,9 @@ " ) from exc\n", "\n", " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", - " client = _lc\n", + " # Wrap in _EmbeddedAdapter so CoordinodeGraph/PropertyGraphStore can call\n", + " # get_schema_text() \u2014 LocalClient has .cypher() only.\n", + " client = _EmbeddedAdapter(_lc)\n", " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 3e93c08..175ae9f 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -223,7 +223,9 @@ " ) from exc\n", "\n", " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", - " client = _lc\n", + " # Wrap in _EmbeddedAdapter so CoordinodeGraph/PropertyGraphStore can call\n", + " # get_schema_text() \u2014 LocalClient has .cypher() only.\n", + " client = _EmbeddedAdapter(_lc)\n", " print(f\"Using embedded LocalClient at {COORDINODE_EMBEDDED_PATH}\")\n" ] }, diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 6a77678..7bc503d 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -20,7 +20,7 @@ "\n", "**Environments:**\n", "- **Google Colab** \u2014 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** \u2014 connects to a running CoordiNode server via gRPC." + "- **Local / Docker Compose** \u2014 set `COORDINODE_ADDR=host:port` to connect to a running CoordiNode server via gRPC; otherwise uses embedded with a file-backed `COORDINODE_EMBEDDED_PATH`." ] }, { diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 51ff79f..a10fe6c 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -697,25 +697,27 @@ def test_text_search_fuzzy(client): def test_cypher_accepts_consistency_kwargs(client): """cypher() wires read_concern / write_concern / read_preference / after_index into the request.""" - # Write with majority concern then read back with majority + primary + after_index. label = f"ConsistencyTest_{uid()}" tag = uid() - client.cypher( - f"CREATE (n:{label} {{tag: $tag, v: 1}})", - params={"tag": tag}, - write_concern="majority", - ) - rows = client.cypher( - f"MATCH (n:{label} {{tag: $tag}}) RETURN n.v AS v", - params={"tag": tag}, - read_concern="majority", - read_preference="primary", - after_index=0, - ) + created = False try: + client.cypher( + f"CREATE (n:{label} {{tag: $tag, v: 1}})", + params={"tag": tag}, + write_concern="majority", + ) + created = True + rows = client.cypher( + f"MATCH (n:{label} {{tag: $tag}}) RETURN n.v AS v", + params={"tag": tag}, + read_concern="majority", + read_preference="primary", + after_index=0, + ) assert rows and rows[0]["v"] == 1 finally: - client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + if created: + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) def test_cypher_rejects_invalid_consistency_values(client): From 8f88a67597c8bf0daa7f8076d5f1201d171ac03b Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sun, 19 Apr 2026 22:54:32 +0300 Subject: [PATCH 16/25] docs(coordinode): note CREATE TEXT INDEX prerequisite for hybrid search example --- coordinode/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coordinode/README.md b/coordinode/README.md index d9c49f0..575319f 100644 --- a/coordinode/README.md +++ b/coordinode/README.md @@ -122,6 +122,10 @@ for r in results: Fuse BM25 full-text and vector similarity using Cypher scoring functions: ```python +# Full-text scoring (text_score / text_match) requires a TEXT INDEX on the +# queried property — without it those calls return zero/no matches. +db.create_text_index("idx_doc_body", "Doc", "body") + # Reciprocal Rank Fusion of text + vector. Projecting `d AS doc_id` returns the # internal node id (an integer) — fetch properties explicitly when needed. rows = db.cypher(""" From 10ab38d917d440571342a560b8841f124d08d18e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 11:07:21 +0300 Subject: [PATCH 17/25] fix(demo,build,tests): always pin SDK in Colab, broaden extension ignores, drop created flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Demo notebooks: pin _coordinode_spec whenever IN_COLAB, regardless of COORDINODE_ADDR. PyPI coordinode can lag the server/proto version used in this demo, so Colab sessions connecting to a remote server were at risk of installing an incompatible SDK. - coordinode-embedded/.gitignore: add *.pyd (Windows) and *.dylib (macOS non-extension builds) alongside *.so so local builds on any platform do not leak into commits. - tests/integration/test_sdk.py::test_cypher_accepts_consistency_kwargs: drop the `created` flag — the DELETE is idempotent (no-op when nothing matches), so unconditional cleanup is simpler and safer. --- coordinode-embedded/.gitignore | 2 ++ demo/notebooks/00_seed_data.ipynb | 2 +- demo/notebooks/01_llama_index_property_graph.ipynb | 2 +- demo/notebooks/02_langchain_graph_chain.ipynb | 2 +- demo/notebooks/03_langgraph_agent.ipynb | 2 +- tests/integration/test_sdk.py | 6 ++---- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/coordinode-embedded/.gitignore b/coordinode-embedded/.gitignore index d91254f..db4b861 100644 --- a/coordinode-embedded/.gitignore +++ b/coordinode-embedded/.gitignore @@ -1,3 +1,5 @@ target/ Cargo.lock *.so +*.pyd +*.dylib diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index d0aeb3f..809bf7a 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -113,7 +113,7 @@ "\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", - " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", + " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 4e3d063..49bc09e 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -103,7 +103,7 @@ "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", - " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", + " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 175ae9f..945a985 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -103,7 +103,7 @@ "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", - " if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\")\n", + " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", "subprocess.run(\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 7bc503d..1065f67 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -102,7 +102,7 @@ " timeout=600,\n", " )\n", "\n", - "_coordinode_spec = _SDK_PIP_SPEC if IN_COLAB and not os.environ.get(\"COORDINODE_ADDR\") else \"coordinode\"\n", + "_coordinode_spec = _SDK_PIP_SPEC if IN_COLAB else \"coordinode\"\n", "subprocess.run(\n", " [\n", " sys.executable,\n", diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index a10fe6c..0096cc0 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -699,14 +699,12 @@ def test_cypher_accepts_consistency_kwargs(client): """cypher() wires read_concern / write_concern / read_preference / after_index into the request.""" label = f"ConsistencyTest_{uid()}" tag = uid() - created = False try: client.cypher( f"CREATE (n:{label} {{tag: $tag, v: 1}})", params={"tag": tag}, write_concern="majority", ) - created = True rows = client.cypher( f"MATCH (n:{label} {{tag: $tag}}) RETURN n.v AS v", params={"tag": tag}, @@ -716,8 +714,8 @@ def test_cypher_accepts_consistency_kwargs(client): ) assert rows and rows[0]["v"] == 1 finally: - if created: - client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) + # DELETE is a no-op when no nodes match — safe to run unconditionally. + client.cypher(f"MATCH (n:{label} {{tag: $tag}}) DELETE n", params={"tag": tag}) def test_cypher_rejects_invalid_consistency_values(client): From 2cd87d3092f5d0112c662f20309846cec38ea793 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 12:58:57 +0300 Subject: [PATCH 18/25] docs(demo): expand seed success message to cover embedded + server modes Previously the final print in 00_seed_data.ipynb only told users to reuse COORDINODE_ADDR, which is misleading for the default embedded workflow. Now lists both paths: open sibling notebooks against the same COORDINODE_EMBEDDED_PATH file, or point them at the same running server via COORDINODE_ADDR. --- demo/notebooks/00_seed_data.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 809bf7a..3bea2a8 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -455,7 +455,9 @@ " print(f\" \u2192 {r['dependency']}\")\n", "\n", "print(\"\\n\u2713 Demo data seeded.\")\n", - "print(\"To query it from notebooks 01\u201303, connect them to the same CoordiNode server (COORDINODE_ADDR).\")\n", + "print(\"To query it from notebooks 01\u201303:\")\n", + "print(\" - Embedded mode: open them with the same COORDINODE_EMBEDDED_PATH (default /content/coordinode-demo.db in Colab, /tmp/coordinode-demo.db locally) \u2014 they will see this seeded graph.\")\n", + "print(\" - Server mode: point them at the same running CoordiNode via COORDINODE_ADDR.\")\n", "client.close()" ] } From 15e6a2055f37c2ff6265e4fc9adcfbe4ebc63359 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 12:59:08 +0300 Subject: [PATCH 19/25] build(demo): bump Colab pin to 2cd87d3092f5d0112c662f20309846cec38ea793 --- 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 3bea2a8..8aa3da1 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -105,14 +105,14 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", " )\n", "\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 49bc09e..d611c73 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -91,7 +91,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# 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", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 945a985..ec9a03a 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -88,7 +88,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# The remaining packages (coordinode, LangChain, etc.) are installed without pins:\n", "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", @@ -115,7 +115,7 @@ " \"-q\",\n", " _coordinode_spec,\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 1065f67..40ec10f 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -43,11 +43,11 @@ "IN_COLAB = \"google.colab\" in sys.modules\n", "_EMBEDDED_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode-embedded\"\n", + " \"@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\"\n", ")\n", "_SDK_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@0b7fd95245f48eab3ae9ddb3e9499171c68a89f4#subdirectory=coordinode\"\n", + " \"@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", From c2d62b0595867651df43c2b3d3fdbec4341d6642 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 13:17:39 +0300 Subject: [PATCH 20/25] fix(demo): stable DEMO_TAG in embedded mode and portable temp dir - 00_seed_data.ipynb DEMO_TAG: use a stable "seed_data" tag in embedded mode so reseeds cleanly replace prior data via the existing DETACH DELETE cleanup. Server mode keeps the UUID-suffixed tag so parallel runs sharing a DB remain isolated. - All 4 notebooks: replace hard-coded "/tmp/coordinode-demo.db" fallback with `os.path.join(tempfile.gettempdir(), "coordinode-demo.db")` so the default path works on native Windows where /tmp does not exist. --- demo/notebooks/00_seed_data.ipynb | 20 ++++++++++++++----- .../01_llama_index_property_graph.ipynb | 10 ++++++---- demo/notebooks/02_langchain_graph_chain.ipynb | 10 ++++++---- demo/notebooks/03_langgraph_agent.ipynb | 10 ++++++---- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index 8aa3da1..fadfae0 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -156,14 +156,16 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import os, tempfile\n", "\n", "# Persistent embedded DB path. Colab has /content which persists across cell\n", - "# reruns within a runtime session; locally fall back to /tmp. Override via\n", - "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "# reruns within a runtime session; locally fall back to the OS temp dir\n", + "# (portable across Linux/macOS/Windows). Override via COORDINODE_EMBEDDED_PATH.\n", "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", " \"COORDINODE_EMBEDDED_PATH\",\n", - " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + " \"/content/coordinode-demo.db\"\n", + " if os.path.isdir(\"/content\")\n", + " else os.path.join(tempfile.gettempdir(), \"coordinode-demo.db\"),\n", ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", @@ -208,7 +210,15 @@ "source": [ "import uuid\n", "\n", - "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or f\"seed_data_{uuid.uuid4().hex[:8]}\"\n", + "# In server mode (COORDINODE_ADDR) parallel runs may share a DB, so a unique\n", + "# UUID tag prevents collisions. In embedded mode each LocalClient has its own\n", + "# file-backed DB, so a stable tag lets reseeds cleanly replace prior data\n", + "# (the DETACH DELETE below is scoped to this tag).\n", + "DEMO_TAG = os.environ.get(\"COORDINODE_DEMO_TAG\") or (\n", + " f\"seed_data_{uuid.uuid4().hex[:8]}\"\n", + " if os.environ.get(\"COORDINODE_ADDR\")\n", + " else \"seed_data\"\n", + ")\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", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index d611c73..9a5cca5 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -189,14 +189,16 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import os, tempfile\n", "\n", "# Persistent embedded DB path. Colab has /content which persists across cell\n", - "# reruns within a runtime session; locally fall back to /tmp. Override via\n", - "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "# reruns within a runtime session; locally fall back to the OS temp dir\n", + "# (portable across Linux/macOS/Windows). Override via COORDINODE_EMBEDDED_PATH.\n", "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", " \"COORDINODE_EMBEDDED_PATH\",\n", - " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + " \"/content/coordinode-demo.db\"\n", + " if os.path.isdir(\"/content\")\n", + " else os.path.join(tempfile.gettempdir(), \"coordinode-demo.db\"),\n", ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index ec9a03a..341fe00 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -190,14 +190,16 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import os, tempfile\n", "\n", "# Persistent embedded DB path. Colab has /content which persists across cell\n", - "# reruns within a runtime session; locally fall back to /tmp. Override via\n", - "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "# reruns within a runtime session; locally fall back to the OS temp dir\n", + "# (portable across Linux/macOS/Windows). Override via COORDINODE_EMBEDDED_PATH.\n", "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", " \"COORDINODE_EMBEDDED_PATH\",\n", - " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + " \"/content/coordinode-demo.db\"\n", + " if os.path.isdir(\"/content\")\n", + " else os.path.join(tempfile.gettempdir(), \"coordinode-demo.db\"),\n", ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 40ec10f..f8a3efc 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -144,14 +144,16 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import os, tempfile\n", "\n", "# Persistent embedded DB path. Colab has /content which persists across cell\n", - "# reruns within a runtime session; locally fall back to /tmp. Override via\n", - "# COORDINODE_EMBEDDED_PATH if you need a different location.\n", + "# reruns within a runtime session; locally fall back to the OS temp dir\n", + "# (portable across Linux/macOS/Windows). Override via COORDINODE_EMBEDDED_PATH.\n", "COORDINODE_EMBEDDED_PATH = os.environ.get(\n", " \"COORDINODE_EMBEDDED_PATH\",\n", - " \"/content/coordinode-demo.db\" if os.path.isdir(\"/content\") else \"/tmp/coordinode-demo.db\",\n", + " \"/content/coordinode-demo.db\"\n", + " if os.path.isdir(\"/content\")\n", + " else os.path.join(tempfile.gettempdir(), \"coordinode-demo.db\"),\n", ")\n", "\n", "if os.environ.get(\"COORDINODE_ADDR\"):\n", From ced47e8e73edfb33e56c12a7dd509ca333a16039 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 13:17:51 +0300 Subject: [PATCH 21/25] build(demo): bump Colab pin to c2d62b0595867651df43c2b3d3fdbec4341d6642 --- 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 fadfae0..d7b17c5 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -105,14 +105,14 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", " )\n", "\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 9a5cca5..4d8866e 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -91,7 +91,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# 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", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 341fe00..285fdcb 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -88,7 +88,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -102,7 +102,7 @@ "# The remaining packages (coordinode, LangChain, etc.) are installed without pins:\n", "# they are pure Python, release frequently, and pip resolves a compatible version.\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", @@ -115,7 +115,7 @@ " \"-q\",\n", " _coordinode_spec,\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index f8a3efc..b9fa8a3 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -43,11 +43,11 @@ "IN_COLAB = \"google.colab\" in sys.modules\n", "_EMBEDDED_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode-embedded\"\n", + " \"@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\"\n", ")\n", "_SDK_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@2cd87d3092f5d0112c662f20309846cec38ea793#subdirectory=coordinode\"\n", + " \"@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", From 50ddc08a89a21fca73be007cb22b57a0054225c3 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 14:28:56 +0300 Subject: [PATCH 22/25] fix(sdk,demo): validate causal-read precondition; clarify embedded install path - cypher() now raises ValueError client-side when after_index > 0 is combined with non-majority write_concern. The server rejects this combination; surfacing it earlier gives a clearer error and matches the documented contract. Unit coverage added. - Notebook ImportError message on missing coordinode-embedded now lists the explicit local install path (git+ pip install from the repo subdirectory, Rust toolchain note, ~5 min build) alongside the Colab install-cell hint and the server-mode escape hatch. - Notebook 00 markdown drops the hard-coded /tmp fallback phrasing; success print shows the runtime COORDINODE_EMBEDDED_PATH verbatim. - Notebook 01/02 comment block now accurately describes coordinode as git-pinned in Colab and unpinned outside, rather than claiming it is always unpinned. --- coordinode/coordinode/client.py | 8 ++++++++ demo/notebooks/00_seed_data.ipynb | 12 ++++++++---- demo/notebooks/01_llama_index_property_graph.ipynb | 12 +++++++++--- demo/notebooks/02_langchain_graph_chain.ipynb | 12 +++++++++--- demo/notebooks/03_langgraph_agent.ipynb | 6 +++++- tests/integration/test_sdk.py | 5 +++++ 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index d6b0db5..2ec9770 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -275,6 +275,14 @@ async def cypher( ExecuteCypherRequest, ) + # Causal reads (after_index > 0) are only satisfiable when writes were + # acknowledged by a majority; otherwise the referenced index may never + # replicate and the read would hang. Mirror the server's rejection. + if after_index is not None and after_index > 0 and (write_concern or "").lower() != "majority": + raise ValueError( + "after_index > 0 requires write_concern='majority' — causal reads " + "depend on majority-committed writes. Pass write_concern='majority'." + ) req = ExecuteCypherRequest( query=query, parameters=dict_to_props(params or {}), diff --git a/demo/notebooks/00_seed_data.ipynb b/demo/notebooks/00_seed_data.ipynb index d7b17c5..df6d781 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -12,7 +12,7 @@ "Populates CoordiNode with a **tech industry knowledge graph**.\n", "\n", "> **Note:** Embedded mode writes to `COORDINODE_EMBEDDED_PATH` (default\n", - "> `/content/coordinode-demo.db` in Colab, `/tmp/coordinode-demo.db` locally), so\n", + "> `/content/coordinode-demo.db` in Colab, the OS temp dir locally), so\n", "> the seeded graph persists across cell reruns and is visible to sibling demo\n", "> notebooks within the same runtime. Delete the file or set a different path to\n", "> reset. Connecting to a real CoordiNode server via `COORDINODE_ADDR` is also\n", @@ -146,7 +146,7 @@ "\n", "- **Colab**: uses `LocalClient(COORDINODE_EMBEDDED_PATH)` \u2014 in-process embedded engine backed by a file under `/content/`, no server required.\n", "- **Local with server**: set `COORDINODE_ADDR=host:port` to point at a running CoordiNode (no auto-probe \u2014 explicit only).\n", - "- **Local without server**: uses `coordinode-embedded` (file-backed at `COORDINODE_EMBEDDED_PATH`, default `/tmp/coordinode-demo.db`). Raises `RuntimeError` with install instructions if the package is missing." + "- **Local without server**: uses `coordinode-embedded` (file-backed at `COORDINODE_EMBEDDED_PATH`, defaulting to the OS temp dir). Raises `RuntimeError` with install instructions if the package is missing." ] }, { @@ -186,7 +186,11 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \"In Colab, rerun the install cell above. \"\n", + " \"Locally, install from source: \"\n", + " \"pip install 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded' \"\n", + " \"(requires Rust toolchain, ~5 min build). \"\n", + " \"Alternatively start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " client = LocalClient(COORDINODE_EMBEDDED_PATH)\n", @@ -466,7 +470,7 @@ "\n", "print(\"\\n\u2713 Demo data seeded.\")\n", "print(\"To query it from notebooks 01\u201303:\")\n", - "print(\" - Embedded mode: open them with the same COORDINODE_EMBEDDED_PATH (default /content/coordinode-demo.db in Colab, /tmp/coordinode-demo.db locally) \u2014 they will see this seeded graph.\")\n", + "print(f\" - Embedded mode: open them with the same COORDINODE_EMBEDDED_PATH (this run: {COORDINODE_EMBEDDED_PATH}) \u2014 they will see this seeded graph.\")\n", "print(\" - Server mode: point them at the same running CoordiNode via COORDINODE_ADDR.\")\n", "client.close()" ] diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index 4d8866e..a61e110 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -99,8 +99,10 @@ "\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", + "# coordinode (SDK) is git-pinned in Colab via _coordinode_spec to keep the\n", + "# SDK/proto in sync with this PR; outside Colab it resolves from PyPI.\n", + "# Other packages (LangChain / llama-index / nest_asyncio) are unpinned \u2014 pure\n", + "# Python, release frequently, pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", " if IN_COLAB\n", @@ -220,7 +222,11 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \"In Colab, rerun the install cell above. \"\n", + " \"Locally, install from source: \"\n", + " \"pip install 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded' \"\n", + " \"(requires Rust toolchain, ~5 min build). \"\n", + " \"Alternatively start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 285fdcb..5213617 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -99,8 +99,10 @@ "# 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", + "# coordinode (SDK) is git-pinned in Colab via _coordinode_spec to keep the\n", + "# SDK/proto in sync with this PR; outside Colab it resolves from PyPI.\n", + "# Other packages (LangChain / llama-index / nest_asyncio) are unpinned \u2014 pure\n", + "# Python, release frequently, pip resolves a compatible version.\n", "_coordinode_spec = (\n", " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", " if IN_COLAB\n", @@ -221,7 +223,11 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \"In Colab, rerun the install cell above. \"\n", + " \"Locally, install from source: \"\n", + " \"pip install 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded' \"\n", + " \"(requires Rust toolchain, ~5 min build). \"\n", + " \"Alternatively start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " _lc = LocalClient(COORDINODE_EMBEDDED_PATH)\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index b9fa8a3..88ae891 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -174,7 +174,11 @@ " except ImportError as exc:\n", " raise RuntimeError(\n", " \"coordinode-embedded is not installed. \"\n", - " \"Run the install cell above, or start a CoordiNode server and set COORDINODE_ADDR.\"\n", + " \"In Colab, rerun the install cell above. \"\n", + " \"Locally, install from source: \"\n", + " \"pip install 'git+https://github.com/structured-world/coordinode-python.git#subdirectory=coordinode-embedded' \"\n", + " \"(requires Rust toolchain, ~5 min build). \"\n", + " \"Alternatively start a CoordiNode server and set COORDINODE_ADDR.\"\n", " ) from exc\n", "\n", " client = LocalClient(COORDINODE_EMBEDDED_PATH)\n", diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index 0096cc0..aa0e79d 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -728,3 +728,8 @@ def test_cypher_rejects_invalid_consistency_values(client): client.cypher("RETURN 1", read_preference="leader") with pytest.raises(ValueError, match="after_index must be a non-negative integer"): client.cypher("RETURN 1", after_index=-1) + # Causal reads (after_index > 0) require write_concern='majority'. + with pytest.raises(ValueError, match="after_index > 0 requires write_concern='majority'"): + client.cypher("RETURN 1", after_index=42) + with pytest.raises(ValueError, match="after_index > 0 requires write_concern='majority'"): + client.cypher("RETURN 1", after_index=42, write_concern="w1") From f6c3f509fdad41ce513ff3e83799a1f1a6c97f07 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 14:29:07 +0300 Subject: [PATCH 23/25] build(demo): bump Colab pin to 50ddc08a89a21fca73be007cb22b57a0054225c3 --- 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 df6d781..23e306c 100644 --- a/demo/notebooks/00_seed_data.ipynb +++ b/demo/notebooks/00_seed_data.ipynb @@ -105,14 +105,14 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", " )\n", "\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/01_llama_index_property_graph.ipynb b/demo/notebooks/01_llama_index_property_graph.ipynb index a61e110..edf7b09 100644 --- a/demo/notebooks/01_llama_index_property_graph.ipynb +++ b/demo/notebooks/01_llama_index_property_graph.ipynb @@ -91,7 +91,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -104,7 +104,7 @@ "# Other packages (LangChain / llama-index / nest_asyncio) are unpinned \u2014 pure\n", "# Python, release frequently, pip resolves a compatible version.\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", diff --git a/demo/notebooks/02_langchain_graph_chain.ipynb b/demo/notebooks/02_langchain_graph_chain.ipynb index 5213617..cf23cf1 100644 --- a/demo/notebooks/02_langchain_graph_chain.ipynb +++ b/demo/notebooks/02_langchain_graph_chain.ipynb @@ -88,7 +88,7 @@ " \"pip\",\n", " \"install\",\n", " \"-q\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode-embedded\",\n", " ],\n", " check=True,\n", " timeout=600,\n", @@ -104,7 +104,7 @@ "# Other packages (LangChain / llama-index / nest_asyncio) are unpinned \u2014 pure\n", "# Python, release frequently, pip resolves a compatible version.\n", "_coordinode_spec = (\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode\"\n", " if IN_COLAB\n", " else \"coordinode\"\n", ")\n", @@ -117,7 +117,7 @@ " \"-q\",\n", " _coordinode_spec,\n", " \"langchain\",\n", - " \"git+https://github.com/structured-world/coordinode-python.git@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=langchain-coordinode\",\n", + " \"git+https://github.com/structured-world/coordinode-python.git@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=langchain-coordinode\",\n", " \"langchain-community\",\n", " \"langchain-openai\",\n", " \"nest_asyncio\",\n", diff --git a/demo/notebooks/03_langgraph_agent.ipynb b/demo/notebooks/03_langgraph_agent.ipynb index 88ae891..6d70466 100644 --- a/demo/notebooks/03_langgraph_agent.ipynb +++ b/demo/notebooks/03_langgraph_agent.ipynb @@ -43,11 +43,11 @@ "IN_COLAB = \"google.colab\" in sys.modules\n", "_EMBEDDED_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode-embedded\"\n", + " \"@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode-embedded\"\n", ")\n", "_SDK_PIP_SPEC = (\n", " \"git+https://github.com/structured-world/coordinode-python.git\"\n", - " \"@c2d62b0595867651df43c2b3d3fdbec4341d6642#subdirectory=coordinode\"\n", + " \"@50ddc08a89a21fca73be007cb22b57a0054225c3#subdirectory=coordinode\"\n", ")\n", "\n", "# Install coordinode-embedded in Colab only (requires Rust build).\n", From 9f86b9330db244b1fe2a148e7a29de9b9dbeb411 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Mon, 20 Apr 2026 14:46:38 +0300 Subject: [PATCH 24/25] style(sdk): strip whitespace when normalizing write_concern for causal-read check --- coordinode/coordinode/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 2ec9770..79104ba 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -278,7 +278,7 @@ async def cypher( # Causal reads (after_index > 0) are only satisfiable when writes were # acknowledged by a majority; otherwise the referenced index may never # replicate and the read would hang. Mirror the server's rejection. - if after_index is not None and after_index > 0 and (write_concern or "").lower() != "majority": + if after_index is not None and after_index > 0 and (write_concern or "").strip().lower() != "majority": raise ValueError( "after_index > 0 requires write_concern='majority' — causal reads " "depend on majority-committed writes. Pass write_concern='majority'." From d62e53b0c5f64f66f79e3c46e8be0b9fa006120e Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Tue, 21 Apr 2026 08:58:00 +0300 Subject: [PATCH 25/25] fix(sdk): validate after_index type before causal-read precondition check --- coordinode/coordinode/client.py | 8 ++++++++ tests/integration/test_sdk.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/coordinode/coordinode/client.py b/coordinode/coordinode/client.py index 79104ba..f65d598 100644 --- a/coordinode/coordinode/client.py +++ b/coordinode/coordinode/client.py @@ -275,6 +275,14 @@ async def cypher( ExecuteCypherRequest, ) + # Validate after_index type/range BEFORE any numeric comparison so that + # True (bool is a subclass of int) and "7" (str) produce a clear + # "must be a non-negative integer" error instead of a misleading + # causal-read violation or a raw TypeError. + if after_index is not None and ( + not isinstance(after_index, int) or isinstance(after_index, bool) or after_index < 0 + ): + raise ValueError(f"after_index must be a non-negative integer, got {after_index!r}") # Causal reads (after_index > 0) are only satisfiable when writes were # acknowledged by a majority; otherwise the referenced index may never # replicate and the read would hang. Mirror the server's rejection. diff --git a/tests/integration/test_sdk.py b/tests/integration/test_sdk.py index aa0e79d..3cf00df 100644 --- a/tests/integration/test_sdk.py +++ b/tests/integration/test_sdk.py @@ -733,3 +733,9 @@ def test_cypher_rejects_invalid_consistency_values(client): client.cypher("RETURN 1", after_index=42) with pytest.raises(ValueError, match="after_index > 0 requires write_concern='majority'"): client.cypher("RETURN 1", after_index=42, write_concern="w1") + # Type validation runs before the causal-read check so bools/strings + # surface the non-negative-integer error rather than a misleading one. + with pytest.raises(ValueError, match="after_index must be a non-negative integer"): + client.cypher("RETURN 1", after_index=True) + with pytest.raises(ValueError, match="after_index must be a non-negative integer"): + client.cypher("RETURN 1", after_index="7") # type: ignore[arg-type]