Skip to content

fix: SDK API correctness — constructor, cypher result, VectorResult fields#2

Open
polaz wants to merge 10 commits intomainfrom
fix/#1-sdk-api-correctness
Open

fix: SDK API correctness — constructor, cypher result, VectorResult fields#2
polaz wants to merge 10 commits intomainfrom
fix/#1-sdk-api-correctness

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 7, 2026

Summary

  • CoordinodeClient("localhost:7080") now works — constructor parses host:port string
  • health() returns bool — tests updated accordingly
  • cypher() returns List[Dict[str, Any]] — LangChain query(), LlamaIndex structured_query(), get(), get_triplets(), get_rel_map() all fixed to use dict row access
  • VectorResult fields: .node.id, .node.properties, .distance (not .node_id, .score)
  • test_types.py: _FakePV vector kind "vector""vector_value"
  • ruff: unused imports removed

Closes #1

- client: support "host:port" string in AsyncCoordinodeClient constructor
- test_types: fix _FakePV vector kind "vector" → "vector_value"
- test_basic: fix health() (bool not dict), cypher() (List[Dict] not .columns/.rows)
- langchain graph.py: query() — cypher() already returns List[Dict], no manual parse
- llama-index base.py: fix get/get_triplets/get_rel_map (dict row access),
  vector_query (r.node.id, r.node.properties, r.distance),
  structured_query (direct return)
- lint: remove unused imports and dead variable

Closes #1
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Connection string support: clients now accept "host:port" format directly.
  • Documentation

    • Added comprehensive READMEs for core, LangChain, and LlamaIndex packages.
  • Refactor

    • Modernized type hints to Python 3.10+ union syntax and built-in generics.
    • Simplified API result access patterns.
  • Chores

    • Updated CI/CD workflows and release automation.
    • Migrated development tooling to uv package manager.
    • Bumped minimum Python support to 3.11.

Walkthrough

Parse "host:port" in client constructor; modernize type hints to PEP 604/3.10+ syntax; change cypher/vector result shape to list-of-row mappings and VectorResult field names; update tests, integrations, docs, CI, build tooling, workspace config; remove runtime proto sys.path insertion.

Changes

Cohort / File(s) Summary
Core types & client
coordinode/_types.py, coordinode/client.py, coordinode/_proto/__init__.py
Modernized type annotations to PEP 604/built-in generics; PyValue and related signatures updated; AsyncCoordinodeClient.__init__ now parses host:port strings and annotates channels as `grpc.aio.Channel
LangChain integration
langchain-coordinode/langchain_coordinode/graph.py
Type hints modernized; CoordinodeGraph.query now returns the client cypher() output (list of row dicts) directly and removed manual columns/rows rematerialization; adjusted signatures/annotations.
Llama‑Index integration
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
Escaped dynamic Cypher identifiers; switched result access to mapping-style (row.get(...)); vector/query result handling updated to use r.node.id, r.node.properties, and r.distance; added NotImplementedError for removed filter combinations.
Tests
tests/unit/test_types.py, tests/integration/test_basic.py
Tests adapted to optional generated proto stubs and renamed proto accessors (vector_value, map_value.entries); integration tests updated for health() returning bool, cypher() returning list-of-dicts, node creation/lookup result access; vector-search test marked xfail.
Workspace, CI & release automation
.github/workflows/ci.yml, .github/workflows/release.yml, .github/workflows/release-please.yml, release-please-config.json, .release-please-manifest.json, pyproject.toml (root), coordinode/pyproject.toml, langchain-coordinode/pyproject.toml, llama-index-coordinode/pyproject.toml
Switched to astral-sh/setup-uv/uv sync/uv run for CI; added workspace root pyproject.toml; bumped minimum Python to >=3.11 across packages; added release-please config, manifest, and workflow; updated release workflow behavior.
Build & Make targets
Makefile
proto now uses python3 -m grpc_tools.protoc; portable post-processing via sed -i.bak across generated *.py then cleanup; install runs uv sync; added install-pip target preserving prior editable pip installs.
Linting & packaging config
ruff.toml, .gitignore
Added ruff.toml (py311 target, line-length 120, exclude proto stubs); .gitignore now ignores coordinode/_proto/ subtree, updated version-file paths, and added GAPS.md.
Documentation & READMEs
README.md, coordinode/README.md, langchain-coordinode/README.md, llama-index-coordinode/README.md
Added/updated package READMEs and quick-starts to use host:port, adjusted cypher/result access examples, expanded dev instructions for both uv and pip flows, and included integration examples for LangChain and LlamaIndex.
Release extras & manifests
release-please-config.json, .release-please-manifest.json, .github/workflows/release-please.yml
Added release-please configuration, manifest, and workflow to automate releases.
Minor formatting & imports
various files
Non-functional import and typing cleanups (replace typing generics with built-ins, remove unused imports), small formatting adjustments across modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: SDK API correctness — constructor, cypher result, VectorResult fields' accurately and concisely describes the primary changes made across multiple SDK components to fix API correctness issues.
Description check ✅ Passed The pull request description directly relates to the changeset, clearly outlining the main fixes (host:port parsing, health() return type, cypher() result format, VectorResult fields, test updates) and providing specific implementation details.
Linked Issues check ✅ Passed The pull request fully addresses all requirements from issue #1: host:port parsing [#1], health() boolean return [#1], cypher() list-of-dicts return type [#1], VectorResult field corrections [#1], _FakePV vector_value fix [#1], and unused import cleanup [#1].
Out of Scope Changes check ✅ Passed The pull request includes supporting changes aligned with issue #1: type hint modernization (Python 3.10+ syntax), workflow/CI updates (uv migration, release-please automation), documentation additions, and version/config adjustments—all reasonable supporting changes for SDK correctness improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/#1-sdk-api-correctness

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@coordinode/_types.py`:
- Line 6: The file coordinode._types has a formatting mismatch around the import
statement (the line importing Any, Dict, List, Union) flagged by ruff; run the
formatter (e.g., `ruff format`) or apply the project's formatting rules to
reformat this file so the import line and file meet ruff's style checks, then
re-run `ruff format --check` to confirm the issue is resolved.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 148-155: The rel_label extraction in get_rel_map() is wrong
because variable-length path [r*1..{depth}] returns relationship dicts, so
change get_rel_map() to either modify its Cypher to include type(r) AS rel_type
(like get_triplets() does) and read row["rel_type"], or keep the existing query
and extract the relationship type from the first relationship dict (e.g., if
rels is a list take rels[0].get("type", "RELATED") instead of str(rels[0]));
update the code that sets rels and rel_label to use the chosen approach (refer
to get_rel_map(), rels, rel_label, and get_triplets() for guidance).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: badaedb1-af49-4cdf-a4e6-30c7a7042df7

📥 Commits

Reviewing files that changed from the base of the PR and between a95c513 and afc7e8b.

📒 Files selected for processing (6)
  • coordinode/_types.py
  • coordinode/client.py
  • langchain-coordinode/langchain_coordinode/graph.py
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
  • tests/integration/test_basic.py
  • tests/unit/test_types.py

@_requires_proto on TestToPropertyValue — skips gracefully without make proto.
TestFromPropertyValue (mock-based) always runs: 7 passed.

Part of #1
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/test_types.py (2)

113-116: ⚠️ Potential issue | 🟠 Major

Use vector_value in the vector assertion.

Line 115 still asserts pv.vector.values, but this suite now targets the vector_value oneof field. This can fail once proto stubs are present and TestToPropertyValue runs.

Suggested fix
     def test_float_list_becomes_vector(self):
         pv = to_property_value([1.0, 2.0, 3.0])
-        assert list(pv.vector.values) == pytest.approx([1.0, 2.0, 3.0])
+        assert list(pv.vector_value.values) == pytest.approx([1.0, 2.0, 3.0])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_types.py` around lines 113 - 116, The test
test_float_list_becomes_vector is asserting pv.vector.values but the proto now
uses the oneof field vector_value; update the assertion to reference
pv.vector_value (e.g., check pv.vector_value.values or pv.vector_value depending
on expected shape) so the test uses the vector_value oneof returned by
to_property_value; locate the assertion in test_float_list_becomes_vector and
replace pv.vector.values with pv.vector_value.* as appropriate.

1-162: ⚠️ Potential issue | 🟡 Minor

CI blocker: file is not Ruff-formatted.

Pipeline reports ruff format --check would reformat this file; please run ruff format before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_types.py` around lines 1 - 162, The file fails the ruff
formatter check; run the auto-formatter and commit the changes: run `ruff
format` (or your project's pre-commit/IDE formatting command) on
tests/unit/test_types.py to reformat the module including classes _FakePV,
_FakeVec, _FakeList, _FakeMap and the test classes TestToPropertyValue and
TestFromPropertyValue so the file passes `ruff format --check` in CI.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tests/unit/test_types.py`:
- Around line 113-116: The test test_float_list_becomes_vector is asserting
pv.vector.values but the proto now uses the oneof field vector_value; update the
assertion to reference pv.vector_value (e.g., check pv.vector_value.values or
pv.vector_value depending on expected shape) so the test uses the vector_value
oneof returned by to_property_value; locate the assertion in
test_float_list_becomes_vector and replace pv.vector.values with
pv.vector_value.* as appropriate.
- Around line 1-162: The file fails the ruff formatter check; run the
auto-formatter and commit the changes: run `ruff format` (or your project's
pre-commit/IDE formatting command) on tests/unit/test_types.py to reformat the
module including classes _FakePV, _FakeVec, _FakeList, _FakeMap and the test
classes TestToPropertyValue and TestFromPropertyValue so the file passes `ruff
format --check` in CI.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 46e07628-7bd6-4749-a08b-a5706939ceca

📥 Commits

Reviewing files that changed from the base of the PR and between afc7e8b and c1a659c.

📒 Files selected for processing (1)
  • tests/unit/test_types.py

polaz added 5 commits April 8, 2026 10:47
- Fix _proto/__init__.py: remove sys.path hack that shadowed
  google namespace package with generated _proto/google/api/
- Fix Makefile proto target: apply import fix to all *.py files
  (not just *_pb2_grpc.py); cypher_pb2.py also had absolute imports
- Fix tests/unit/test_types.py: correct field names (vector_value,
  map_value.entries); test_none → null semantics, not raises
- Fix tests/integration/test_basic.py: use RETURN n.name (id(n) not
  implemented in alpha); mark test_vector_search as xfail
- ci.yml: switch to uv, add Python 3.13 to matrix, remove 3.9/3.10
  (both past EOL Oct 2025)
- pyproject.toml (all 3 packages): requires-python >=3.11, add 3.13
  classifier, drop 3.9/3.10
- ruff.toml: exclude coordinode/_proto/ from linting (generated code)
- Auto-fix ruff UP violations: Union→X|Y, Dict/List→dict/list,
  Optional→X|None, isinstance tuples→X|Y union syntax

Tests: 21 passed, 1 xfailed (vector search — alpha server limitation)
VectorServiceImpl is a stub in server/src/services/vector.rs that
always returns []. HNSW algorithm is implemented (coordinode-vector
crate) but not wired to the RPC handler. Tracked as G007 in GAPS.md.
uv workspace:
- Root pyproject.toml with [tool.uv.workspace] — members: coordinode,
  langchain-coordinode, llama-index-coordinode
- uv.lock committed (89 packages, reproducible installs)
- hatch-vcs: fallback-version = "0.0.0" + raw-options root = ".."
  so git version detection works from subdirectory
- Makefile: `make install` now uses `uv sync`; `make install-pip`
  as pip fallback for non-uv contributors
- CI updated: `uv sync` + `uv run` everywhere, no `uv pip install`

Both pip and uv work without any wrapper:
  pip install coordinode           # PyPI user
  uv add coordinode                # uv user
  pip install -e coordinode/       # contributor (pip)
  uv sync                          # contributor (uv)

READMEs:
- Root README.md: badges, all 3 packages, uv+pip quickstart, dev
  setup (uv + pip), donation section with QR code
- coordinode/README.md: PyPI page — connection options, type
  mapping table, vector search, async client
- langchain-coordinode/README.md: PyPI page — GraphCypherQAChain,
  schema inspection, LLMGraphTransformer, add_graph_documents
- llama-index-coordinode/README.md: PyPI page — PropertyGraphIndex
  from docs + from existing, vector+keyword hybrid retrieval,
  manual upsert, capabilities table
- assets/usdt-qr.svg: USDT TRC-20 donation QR code (TFDsez...)
- Sub-package READMEs reference QR via absolute GitHub raw URL
  (required for PyPI markdown rendering)
- readme = "README.md" in each pyproject.toml (PyPI page)

python-version classifiers: 3.11, 3.12, 3.13 (3.9/3.10 EOL)
requires-python = ">=3.11" in all 3 packages
- Add release-please workflow + config + manifest (initial version 0.3.0)
- release.yml: remove TestPyPI job, publish all tags directly to PyPI
- coordinode/pyproject.toml: fix version-file path (_version.py, not coordinode/_version.py)
- .gitignore: cover entire _proto/ dir and fix _version.py paths for subdirs
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 8, 2026

@coderabbitai Re: outside-diff comments on tests/unit/test_types.py:

  • vector assertion (lines 113–116): already uses pv.vector_value.values (line 118) — stale finding from earlier diff.
  • ruff format (lines 1–162): ruff format --check reports the file is already formatted — stale finding.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@polaz, thanks for the corrections — both findings are stale and I apologize for the noise.

  • vector assertion: The code already correctly uses pv.vector_value.values; my comment was based on an outdated view of the diff.
  • ruff format: The file is already properly formatted per ruff format --check; the formatting comment should not have been raised.

I'll resolve both comments now.


🏁 Script executed:

#!/bin/bash
# List review comments by coderabbitai on PR `#2` to find the ones to resolve
gh api repos/structured-world/coordinode-python/pulls/2/comments \
  --jq '.[] | select(.user.login == "coderabbitai") | {id: .id, body: .body[0:120]}'

Length of output: 0


Let me try a different endpoint to locate those comments.


🏁 Script executed:

#!/bin/bash
# List all reviews on PR `#2`
gh api repos/structured-world/coordinode-python/pulls/2/reviews \
  --jq '.[] | select(.user.login == "coderabbitai") | {id: .id, state: .state, submitted_at: .submitted_at}'

# Also try issue comments
gh api repos/structured-world/coordinode-python/issues/2/comments \
  --jq '.[] | select(.user.login == "coderabbitai") | {id: .id, body: .body[0:150]}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check all review comments on PR `#2` without user filter
gh api repos/structured-world/coordinode-python/pulls/2/comments \
  --jq '.[0:3] | .[] | {id: .id, user: .user.login, body: .body[0:100]}'

Length of output: 500


🏁 Script executed:

#!/bin/bash
# Get all coderabbitai[bot] review comments and find the ones about test_types.py
gh api repos/structured-world/coordinode-python/pulls/2/comments \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, path: .path, body: .body[0:200]}'

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Check review bodies for mentions of test_types.py
gh api repos/structured-world/coordinode-python/pulls/2/reviews \
  --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, state: .state, body_preview: .body[0:300]}'

Length of output: 961


@polaz, confirmed — both findings are stale and I apologize for the false positives. I've located them in review #4071440333 as "outside-diff range" comments embedded in the review body rather than as standalone inline comments, so they can't be individually resolved via the GitHub API.

To summarize what was wrong:

  • vector assertion (lines 113–116): The code already correctly accesses pv.vector_value.values — my comment was based on a stale snapshot of the diff.
  • ruff format (lines 1–162): tests/unit/test_types.py is already properly formatted per ruff format --check — the finding should not have been raised.

Both are incorrect findings and can be safely disregarded. I'll make a note to be more careful about flagging outside-diff issues on files that haven't actually changed in the PR.


✏️ Learnings added
Learnt from: polaz
URL: https://github.com/structured-world/coordinode-python/pull/2

Timestamp: 2026-04-08T10:40:11.495Z
Learning: In the coordinode-python repository, "outside-diff range" review comments posted in the review body (not as inline PR comments) cannot be individually resolved via the GitHub API. These appear in the second review pass when lines fall outside the current diff.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@polaz polaz requested review from Copilot and removed request for Copilot April 8, 2026 10:41
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci.yml (1)

53-53: 🧹 Nitpick | 🔵 Trivial

Pin the integration service image to a fixed tag or digest.

Using ghcr.io/structured-world/coordinode:latest can cause flaky CI when upstream changes land. Prefer a version tag or immutable digest for reproducible runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 53, The workflow currently references a
mutable image tag "image: ghcr.io/structured-world/coordinode:latest"; replace
that with a fixed version tag or an immutable digest (e.g.,
"ghcr.io/structured-world/coordinode:<version>" or
"ghcr.io/structured-world/coordinode@sha256:<digest>") to ensure reproducible CI
runs—update the CI job definition that contains the `image:` key to use the
chosen tag or digest and, if using a digest, obtain it from the registry (or a
release) and paste the exact sha256 value.
tests/unit/test_types.py (1)

38-41: ⚠️ Potential issue | 🟡 Minor

_FakeMap.fields doesn't match expected .entries accessor.

The _FakeMap class uses .fields but from_property_value (line 74 in _types.py) accesses .map_value.entries. While _FakeMap isn't currently used in the TestFromPropertyValue tests, this inconsistency could cause issues if map tests are added.

🐛 Suggested fix
 class _FakeMap:
     def __init__(self, fields):
-        self.fields = dict(fields)
+        self.entries = dict(fields)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test_types.py` around lines 38 - 41, The _FakeMap test helper
exposes a .fields attribute but the code under test (from_property_value)
expects a .map_value.entries accessor; update _FakeMap so it mimics the real
protobuf structure by providing a .map_value object with an .entries mapping (or
a .map_value property that returns an object with an .entries attribute) instead
of or in addition to .fields so tests using from_property_value will find
.map_value.entries as expected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release-please.yml:
- Around line 15-17: The workflow exposes outputs release_created and tag_name
via outputs: release_created: ${{ steps.rp.outputs.release_created }} and
tag_name: ${{ steps.rp.outputs.tag_name }} but no downstream job consumes them;
either remove these unused outputs from the outputs block or add/document the
intended consumer (e.g., a downstream job or an external workflow like
release.yml that triggers on tag push) so the outputs are actually used; update
the outputs block accordingly and, if keeping them, add a comment or README
describing how release.yml or other workflows should consume
steps.rp.outputs.release_created and steps.rp.outputs.tag_name.

In @.github/workflows/release.yml:
- Line 98: Remove the extra trailing blank line at the end of release.yml that
triggers YAML lint's empty-lines error; open .github/workflows/release.yml (the
release workflow file) and delete the final blank line so the file ends with the
last YAML content line (optionally followed by a single newline character) and
then save.

In `@coordinode/client.py`:
- Around line 104-107: The current host:port parsing in coordinode/client.py
using host.rsplit(":", 1) will break IPv6 addresses; update the block that
handles the "host:port" single-string (the host and port variables) to first
detect bracketed IPv6 form (e.g., "[::1]:7080") and extract the address between
'[' and ']' and the trailing port, otherwise only split on the last colon for
non-bracketed hosts but ensure you skip raw IPv6 without brackets (e.g.,
"2001:db8::1") by requiring bracket notation or rejecting/ignoring a port when
host contains multiple colons; implement this logic in the same place where host
and port are reassigned so existing callers (host, port) remain unchanged and
add a short comment documenting that unbracketed IPv6 must be passed as separate
host/port or use bracket notation.

In `@coordinode/pyproject.toml`:
- Around line 49-50: The configured VCS hook generates _version.py but the
package still hardcodes __version__ = "0.3.0a1"; update the package to use the
generated version by changing the __version__ assignment in
coordinode/__init__.py to import __version__ from _version.py with a safe
ImportError fallback, or remove the version-file = "_version.py" hatch config if
you intend to keep manual versioning; specifically modify the module-level
__version__ definition to attempt "from ._version import __version__" (fallback
to a sensible default) so the generated file is actually used.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 62-64: In the get() method, ids is typed as list[str] but each
node_id is passed directly to self._client.get_node(node_id) which expects an
int; convert each node_id to int before calling get_node (e.g., call
self._client.get_node(int(node_id))) and ensure any downstream handling uses the
integer result accordingly.

In `@Makefile`:
- Line 1: The .PHONY declaration is out of sync: it lists install-uv but the
actual Makefile target is install-pip (and checkmake suggests adding an all
target). Update the .PHONY line to include install-pip instead of install-uv and
(optionally) add all to the .PHONY list so it matches the defined targets;
ensure the identifiers mentioned (.PHONY, install-uv, install-pip, all) map
exactly to the targets present elsewhere in the file.
- Around line 19-20: The sed invocation in the Makefile is using BSD-style
in-place editing (`sed -i ''`) which breaks on GNU sed; update the command that
targets generated Python files in $(PROTO_OUT) (the line with "sed -i '' 's/from
coordinode\./from coordinode._proto.coordinode./g'") to use a portable two-step
approach: run sed with a safe backup suffix (e.g. `-i.bak`) and then remove the
generated .bak files after the substitution to avoid leaving artifacts; ensure
the replacement string and target file selection remain identical so the
substitution and subsequent cleanup operate on the same files.

In `@tests/integration/test_basic.py`:
- Line 52: The compound assertion using found and found[0]["name"] should be
split so failures indicate which condition failed: first assert that the search
results exist (e.g., assert found or assert len(found) > 0 with a clear
message), then assert the first item's name equals "sdk-test-node" (assert
found[0]["name"] == "sdk-test-node"), referencing the found variable and the
index access found[0]["name"] to locate the check.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Line 53: The workflow currently references a mutable image tag "image:
ghcr.io/structured-world/coordinode:latest"; replace that with a fixed version
tag or an immutable digest (e.g.,
"ghcr.io/structured-world/coordinode:<version>" or
"ghcr.io/structured-world/coordinode@sha256:<digest>") to ensure reproducible CI
runs—update the CI job definition that contains the `image:` key to use the
chosen tag or digest and, if using a digest, obtain it from the registry (or a
release) and paste the exact sha256 value.

In `@tests/unit/test_types.py`:
- Around line 38-41: The _FakeMap test helper exposes a .fields attribute but
the code under test (from_property_value) expects a .map_value.entries accessor;
update _FakeMap so it mimics the real protobuf structure by providing a
.map_value object with an .entries mapping (or a .map_value property that
returns an object with an .entries attribute) instead of or in addition to
.fields so tests using from_property_value will find .map_value.entries as
expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6a174617-f7ca-43af-a3b8-a064fe0350df

📥 Commits

Reviewing files that changed from the base of the PR and between c1a659c and ac43ad0.

⛔ Files ignored due to path filters (2)
  • assets/usdt-qr.svg is excluded by !**/*.svg
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • .github/workflows/ci.yml
  • .github/workflows/release-please.yml
  • .github/workflows/release.yml
  • .gitignore
  • .release-please-manifest.json
  • Makefile
  • README.md
  • coordinode/README.md
  • coordinode/_proto/__init__.py
  • coordinode/_types.py
  • coordinode/client.py
  • coordinode/pyproject.toml
  • langchain-coordinode/README.md
  • langchain-coordinode/langchain_coordinode/graph.py
  • langchain-coordinode/pyproject.toml
  • llama-index-coordinode/README.md
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
  • llama-index-coordinode/pyproject.toml
  • pyproject.toml
  • release-please-config.json
  • ruff.toml
  • tests/integration/test_basic.py
  • tests/unit/test_types.py
💤 Files with no reviewable changes (1)
  • coordinode/_proto/init.py

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aligns the Python SDK + LangChain/LlamaIndex integrations and tests with the actual gRPC API behavior (constructor address parsing, health() return type, cypher() row shape, VectorResult fields), and modernizes repo tooling (uv workspace, ruff config, release-please automation, refreshed READMEs/CI).

Changes:

  • Fix SDK surface/API expectations across core client, integrations, and tests (health(), cypher(), VectorResult, host:port handling).
  • Add/refresh developer tooling and automation (uv workspace root, ruff config, release-please, updated CI/release workflows).
  • Expand and split documentation into per-package READMEs and improve top-level README.

Reviewed changes

Copilot reviewed 22 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unit/test_types.py Updates PropertyValue conversion tests, adds proto-stub detection + skipping, aligns field names.
tests/integration/test_basic.py Updates integration tests to new health() and cypher() return shapes; xfails vector search until server wiring exists.
ruff.toml Adds ruff configuration and excludes generated proto stubs.
release-please-config.json Adds release-please config for Python releases.
README.md Refreshes top-level docs (badges, examples, dev setup, integration test instructions, support section).
pyproject.toml Adds uv workspace root + shared dev dependency groups.
Makefile Updates proto generation/install targets and switches to uv-based install path.
llama-index-coordinode/README.md Adds package-specific README with usage examples and capabilities.
llama-index-coordinode/pyproject.toml Updates Python requirement to 3.11+, readme location, hatch-vcs options/classifiers.
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py Fixes integration logic to match cypher() dict rows and VectorResult fields; updates typing.
langchain-coordinode/README.md Adds package-specific README with usage examples and API notes.
langchain-coordinode/pyproject.toml Updates Python requirement to 3.11+, readme location, hatch-vcs options/classifiers.
langchain-coordinode/langchain_coordinode/graph.py Updates query() to return cypher()’s dict rows directly; typing cleanups.
coordinode/README.md Adds package-specific README for the core SDK with updated examples and API notes.
coordinode/pyproject.toml Updates Python requirement to 3.11+, hatch-vcs config/version file path, classifiers.
coordinode/client.py Implements host:port parsing, updates return types, aligns VectorResult fields, typing cleanup.
coordinode/_types.py Updates type aliases and PropertyValue conversion logic (but contains a runtime isinstance issue).
coordinode/_proto/init.py Removes sys.path mutation logic for generated stubs.
assets/usdt-qr.svg Adds donation QR asset referenced by READMEs.
.release-please-manifest.json Adds release-please manifest with initial version.
.gitignore Adjusts ignores for generated proto stubs and hatch-vcs version files; adds GAPS.md ignore.
.github/workflows/release.yml Updates release pipeline to use release-please + uv and simplifies publishing flow.
.github/workflows/release-please.yml Adds release-please workflow definition.
.github/workflows/ci.yml Switches CI to uv, updates Python matrix to 3.11–3.13, runs proto generation/tests via uv.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py (2)

80-105: ⚠️ Potential issue | 🟠 Major

These public filter arguments still silently no-op.

get_triplets() never consumes properties/ids, and delete() never consumes relation_names/properties. Callers supplying those arguments currently get a successful call with the wrong result set. Please either implement the predicates or fail fast with NotImplementedError for unsupported combinations.

Also applies to: 188-201

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 80 - 105, get_triplets currently ignores the properties and ids parameters
and delete ignores relation_names and properties, causing silent no-ops; update
get_triplets (function get_triplets) to either apply filters for properties and
ids into the Cypher WHERE clause (e.g., add predicates against node properties
and id(n)/id(m) using params) or explicitly raise NotImplementedError when those
args are provided, and similarly update delete (method delete) to either
incorporate relation_names and properties into the deletion MATCH/WHERE pattern
or raise NotImplementedError if those combinations are unsupported; ensure you
reference and populate the params dict and adjust rel_pattern/where construction
consistently so callers get a correct filtered result or a clear failure.

123-156: ⚠️ Potential issue | 🟠 Major

ignore_rels is accepted but never applied.

get_rel_map(..., ignore_rels=[...]) still returns paths containing those relationship types because the parameter only appears in a comment. That breaks callers relying on the exclusion filter. Filter r in Cypher (ALL(rel IN r WHERE type(rel) NOT IN $ignore_rels)) or skip matching rows before building the Relation.

Example Python-side guard
         result = self._client.cypher(cypher, params={"ids": ids})
+        ignored = set(ignore_rels or [])
 
         triplets: list[list[LabelledNode]] = []
         for row in result:
+            rels = row.get("r", [])
+            if ignored and isinstance(rels, list):
+                if any(isinstance(rel, dict) and rel.get("type") in ignored for rel in rels):
+                    continue
             src_data = row.get("n", {})
             dst_data = row.get("m", {})
             src_id = str(row.get("_src_id", ""))
             dst_id = str(row.get("_dst_id", ""))
-            # Variable-length path [r*1..N] returns a list of relationship dicts.
-            rels = row.get("r", [])
             if isinstance(rels, list) and rels:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 123 - 156, The ignore_rels parameter of get_rel_map is never used; update
the cypher built in get_rel_map (function get_rel_map in base.py) to include a
WHERE clause filtering variable-length relationships, e.g. add "AND ALL(rel IN r
WHERE type(rel) NOT IN $ignore_rels)" when ignore_rels is provided and pass
ignore_rels in params, or alternatively after fetching result rows filter out
any row whose rels (row["r"]) contains a relationship whose type is in
ignore_rels before constructing LabelledNode triplets; ensure you reference ids
and _src_id/_dst_id logic unchanged and only apply the ignore_rels check to the
rels list.
♻️ Duplicate comments (1)
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py (1)

62-64: ⚠️ Potential issue | 🔴 Critical

get(ids=...) still assumes callers already have graph-internal integer IDs.

ids is declared as list[str], but Line 64 forwards each value straight into get_node(). This is the same blocker from the earlier review: the client API expects the integer graph ID, so this branch is still brittle and will mis-handle the string IDs exposed by the adapter. Convert here or switch the lookup to the persisted n.id property instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 62 - 64, The get(ids=...) branch in the get method incorrectly forwards
string IDs to self._client.get_node; convert each incoming string adapter ID to
the internal integer graph ID before calling get_node (or resolve via the
persisted node identity like n.id) so the client receives the expected integer
ID; update the loop that iterates over ids to perform the lookup/translation
(e.g., map adapter ID -> internal ID) and then call self._client.get_node with
that integer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 68-70: The Cypher is vulnerable because property keys, node
labels, and relationship types are interpolated directly; create a small helper
(e.g., escape_cypher_identifier(name)) that validates and safely
backtick-escapes identifiers (reject or sanitize names containing backticks,
spaces, colons, or other illegal chars), then use that helper when building
where_clauses (wrapping each property key as `n.{escaped_key}`) and when
composing any label/relationship/type patterns in get_triplets() (apply to
relation_names and any label interpolations) so only sanitized, quoted
identifiers are injected while keeping property values as parameters.

---

Outside diff comments:
In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 80-105: get_triplets currently ignores the properties and ids
parameters and delete ignores relation_names and properties, causing silent
no-ops; update get_triplets (function get_triplets) to either apply filters for
properties and ids into the Cypher WHERE clause (e.g., add predicates against
node properties and id(n)/id(m) using params) or explicitly raise
NotImplementedError when those args are provided, and similarly update delete
(method delete) to either incorporate relation_names and properties into the
deletion MATCH/WHERE pattern or raise NotImplementedError if those combinations
are unsupported; ensure you reference and populate the params dict and adjust
rel_pattern/where construction consistently so callers get a correct filtered
result or a clear failure.
- Around line 123-156: The ignore_rels parameter of get_rel_map is never used;
update the cypher built in get_rel_map (function get_rel_map in base.py) to
include a WHERE clause filtering variable-length relationships, e.g. add "AND
ALL(rel IN r WHERE type(rel) NOT IN $ignore_rels)" when ignore_rels is provided
and pass ignore_rels in params, or alternatively after fetching result rows
filter out any row whose rels (row["r"]) contains a relationship whose type is
in ignore_rels before constructing LabelledNode triplets; ensure you reference
ids and _src_id/_dst_id logic unchanged and only apply the ignore_rels check to
the rels list.

---

Duplicate comments:
In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 62-64: The get(ids=...) branch in the get method incorrectly
forwards string IDs to self._client.get_node; convert each incoming string
adapter ID to the internal integer graph ID before calling get_node (or resolve
via the persisted node identity like n.id) so the client receives the expected
integer ID; update the loop that iterates over ids to perform the
lookup/translation (e.g., map adapter ID -> internal ID) and then call
self._client.get_node with that integer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 467f8d62-23c3-4fc2-878a-5e5a6159ffad

📥 Commits

Reviewing files that changed from the base of the PR and between ac43ad0 and bc156c3.

📒 Files selected for processing (1)
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py

…ifiers

- add _cypher_ident(): backtick-escapes labels, rel-types, property keys
- get(): lookup nodes by n.id (string) instead of get_node() (expects int)
- get(): escape property keys in WHERE clause
- get_triplets(): escape relation_names; raise NotImplementedError for properties/ids
- upsert_relations(): escape rel.label
- get_rel_map(): implement ignore_rels Python-side filter
- delete(): raise NotImplementedError for relation_names/properties

fix(client): parse host:port regardless of default port value

fix(build): portable sed -i.bak instead of macOS-only sed -i ''

fix(build): sync .PHONY — install-uv to install-pip

fix(release): remove unused outputs from release-please.yml

fix(release): remove trailing blank line in release.yml

feat(coordinode): import __version__ from hatch-vcs _version.py

test(integration): split compound assert for clearer failure messages
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 8, 2026

@coderabbitai Re: outside-diff findings on base.py:

  • get_triplets ignores properties/ids (80-105); delete ignores relation_names/properties (188-201): Fixed — both methods now raise NotImplementedError when unsupported params are provided, so callers get an explicit error instead of silent no-op.
  • ignore_rels not applied in get_rel_map (123-156): Fixed — implemented Python-side filter that skips rows where any relationship in the path has a type in ignore_rels.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz, thanks for the updates — both fixes look like the right approach:

  • NotImplementedError for unsupported params: Raising explicitly on get_triplets (unsupported ids/properties) and delete (unsupported relation_names/properties) is much safer than a silent no-op. Callers will now get a clear signal instead of wondering why nothing happened.
  • ignore_rels filter in get_rel_map: A Python-side post-filter on the result rows is a clean solution — it keeps the Cypher query simple while still correctly excluding unwanted relationship types from the returned map.

Appreciate the thorough follow-through on those findings!


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-python PR: 0
File: :0-0
Timestamp: 2026-04-08T10:40:11.495Z
Learning: In the coordinode-python repository, "outside-diff range" review comments posted in the review body (not as inline PR comments) cannot be individually resolved via the GitHub API. These appear in the second review pass when lines fall outside the current diff.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py (1)

215-222: ⚠️ Potential issue | 🟡 Minor

Inconsistent ID semantics between get() and delete().

get() queries by the n.id property (string adapter ID), but delete() uses id(n) (internal graph integer ID). If callers pass the same ids list to both methods, delete() will target different nodes.

Consider aligning delete() to also use n.id IN $ids:

♻️ Suggested alignment
         if ids:
-            cypher = "MATCH (n) WHERE id(n) IN $ids DETACH DELETE n"
+            cypher = "MATCH (n) WHERE n.id IN $ids DETACH DELETE n"
             self._client.cypher(cypher, params={"ids": ids})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 215 - 222, The delete() implementation in base.py uses the Neo4j internal
integer id(n) but get() filters by the node property n.id (string adapter ID),
causing mismatch; update the delete() method (the branch that handles ids) to
filter on the node property (n.id IN $ids) instead of id(n) and pass the same
params key (ids) so callers can use identical id lists for get() and delete().
coordinode/client.py (1)

289-299: 🧹 Nitpick | 🔵 Trivial

health() swallows all gRPC errors — consider logging or distinguishing transient failures.

Returning False for any grpc.RpcError is reasonable for a simple health check, but transient network issues become indistinguishable from a genuinely unhealthy server. Consider adding debug-level logging or documenting that False means "unreachable or unhealthy."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coordinode/client.py` around lines 289 - 299, The health() method currently
swallows all grpc.RpcError and returns False; change it to catch grpc.RpcError
as e and emit a debug-level log including the exception, its gRPC status code
(e.code()) and details (e.details()) to distinguish transient network errors
from a true unhealthy service, e.g., inside health() around the
_health_stub.Check call log with logger.debug("health check failed", exc_info=e,
status=e.code(), details=e.details()); if no module logger exists, create one
with logging.getLogger(__name__).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release-please.yml:
- Around line 16-20: The release config only targets the root package; update
release-please-config.json by adding three package entries under the "packages"
object for coordinode, langchain-coordinode, and llama-index-coordinode (each
pointing to the correct package directory and including the desired
release-type/rules as the root entry uses) so release-please will create
separate release PRs/tags per workspace package; if you intentionally want a
single shared root version instead, add a short comment in
release-please-config.json (or the repo README) stating that design decision to
avoid confusion.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 182-188: The upsert_nodes method interpolates label directly into
the Cypher string causing a potential injection vulnerability; change
upsert_nodes to escape the node label using the existing _cypher_ident helper
(call _cypher_ident(_node_label(node)) or equivalent) when building the cypher
variable and keep params for id and props as before so labels are safely
quoted/escaped while preserving the MERGE/SET logic in upsert_nodes.

---

Outside diff comments:
In `@coordinode/client.py`:
- Around line 289-299: The health() method currently swallows all grpc.RpcError
and returns False; change it to catch grpc.RpcError as e and emit a debug-level
log including the exception, its gRPC status code (e.code()) and details
(e.details()) to distinguish transient network errors from a true unhealthy
service, e.g., inside health() around the _health_stub.Check call log with
logger.debug("health check failed", exc_info=e, status=e.code(),
details=e.details()); if no module logger exists, create one with
logging.getLogger(__name__).

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 215-222: The delete() implementation in base.py uses the Neo4j
internal integer id(n) but get() filters by the node property n.id (string
adapter ID), causing mismatch; update the delete() method (the branch that
handles ids) to filter on the node property (n.id IN $ids) instead of id(n) and
pass the same params key (ids) so callers can use identical id lists for get()
and delete().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 66312667-a2cf-47d0-b6c6-3f617f2b61a3

📥 Commits

Reviewing files that changed from the base of the PR and between bc156c3 and 1c64499.

📒 Files selected for processing (8)
  • .github/workflows/release-please.yml
  • .github/workflows/release.yml
  • Makefile
  • coordinode/__init__.py
  • coordinode/_types.py
  • coordinode/client.py
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
  • tests/integration/test_basic.py

fix(llama-index): align delete() to use n.id (string) not id(n) (int graph ID)

fix(client): add debug logging on gRPC health check failure

docs(release): document single shared version intent in release-please-config.json
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 8, 2026

@coderabbitai Re: outside-diff findings in review:

  • delete() id semantics (base.py:215-222): Fixed — delete() now uses n.id IN $ids (string adapter ID), consistent with get() which also uses n.id.
  • health() logging (client.py:289-299): Fixed — added logger.debug() call with error code and details so gRPC errors are not silently swallowed.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz, thanks for the follow-through on both — both are solid fixes:

  • delete() id semantics: Using n.id IN $ids keeps the deletion path consistent with get(), ensuring the same string adapter ID field is used throughout rather than graph-internal node IDs.
  • health() logging: Surfacing the gRPC error code and details via logger.debug() makes silent swallowing a thing of the past — much easier to diagnose connection issues in production.

🧠 Learnings used
Learnt from: polaz
Repo: structured-world/coordinode-python PR: 0
File: :0-0
Timestamp: 2026-04-08T10:40:11.495Z
Learning: In the coordinode-python repository, "outside-diff range" review comments posted in the review body (not as inline PR comments) cannot be individually resolved via the GitHub API. These appear in the second review pass when lines fall outside the current diff.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 26 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +107 to +114
# Support "host:port" as a single string (common gRPC convention).
# Parse whenever the last colon-delimited segment is numeric, regardless
# of default port. IPv6 bracket notation ([::1]:7080) is handled correctly
# by rsplit(":", 1): "[::1]" + "7080".
if ":" in host:
_h, _p = host.rsplit(":", 1)
if _p.isdigit():
host, port = _h, int(_p)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host:port parsing will mis-handle unbracketed IPv6 addresses (e.g. 2001:db8::1): the last segment ("1") is numeric so it will be treated as the port, changing host/port and producing an incorrect gRPC target. Consider only parsing when the input matches ^[^:]+:\d+$ (single-colon hostnames) or the bracketed IPv6 form ^\[.*\]:\d+$, and otherwise leave host untouched.

Suggested change
# Support "host:port" as a single string (common gRPC convention).
# Parse whenever the last colon-delimited segment is numeric, regardless
# of default port. IPv6 bracket notation ([::1]:7080) is handled correctly
# by rsplit(":", 1): "[::1]" + "7080".
if ":" in host:
_h, _p = host.rsplit(":", 1)
if _p.isdigit():
host, port = _h, int(_p)
import re
# Support "host:port" as a single string (common gRPC convention), but
# only for unambiguous forms:
# - single-colon hostnames/IPv4: "localhost:7080", "127.0.0.1:7080"
# - bracketed IPv6: "[::1]:7080"
# Leave other colon-containing inputs, such as unbracketed IPv6 literals
# like "2001:db8::1", untouched.
if re.fullmatch(r"[^:]+:\d+", host):
_h, _p = host.split(":", 1)
host, port = _h, int(_p)
elif re.fullmatch(r"\[.*\]:\d+", host):
_h, _p = host.rsplit(":", 1)
host, port = _h, int(_p)

Copilot uses AI. Check for mistakes.
Comment on lines +81 to 83
where_clauses = " AND ".join(f"n.{_cypher_ident(k)} = ${k}" for k in properties)
cypher = f"MATCH (n) WHERE {where_clauses} RETURN n, n.id AS _nid LIMIT 1000"
result = self._client.cypher(cypher, params=properties)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the properties branch, property keys are identifier-escaped via _cypher_ident(k), but the parameter name still uses the raw key (${k}). If a key contains characters that require backticks (spaces, hyphens, etc.), the Cypher will still fail because $... parameter names must be valid identifiers. Consider generating safe parameter names (e.g. p0, p1, …) and building a separate params dict mapping those to the original values.

Suggested change
where_clauses = " AND ".join(f"n.{_cypher_ident(k)} = ${k}" for k in properties)
cypher = f"MATCH (n) WHERE {where_clauses} RETURN n, n.id AS _nid LIMIT 1000"
result = self._client.cypher(cypher, params=properties)
params: dict[str, Any] = {}
clauses: list[str] = []
for i, (k, v) in enumerate(properties.items()):
param_name = f"p{i}"
clauses.append(f"n.{_cypher_ident(k)} = ${param_name}")
params[param_name] = v
where_clauses = " AND ".join(clauses)
cypher = f"MATCH (n) WHERE {where_clauses} RETURN n, n.id AS _nid LIMIT 1000"
result = self._client.cypher(cypher, params=params)

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 188
props = _labelled_to_props(node)
label = _node_label(node)
cypher = (
f"MERGE (n:{label} {{id: $id}}) "
"SET n += $props"
)
label = _cypher_ident(_node_label(node))
cypher = f"MERGE (n:{label} {{id: $id}}) SET n += $props"
self._client.cypher(cypher, params={"id": node.id, "props": props})
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upsert_nodes() interpolates label directly into the Cypher (MERGE (n:{label} ...)). Since labels can originate from upstream inputs (e.g. EntityNode.label), this allows Cypher injection / query breakage if the label contains special characters. Use _cypher_ident(label) (or otherwise validate/whitelist labels) before embedding it into the query string.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +66
" HNSW algorithm (coordinode-vector crate) is implemented, but not wired to the RPC handler."
" Tracked as gap G007 in coordinode-python GAPS.md."
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The xfail reason references coordinode-python GAPS.md (and a G007 gap), but this repo doesn’t include that document (and it’s also gitignored). Consider pointing to a public issue/PR or the upstream CoordiNode server tracking item instead, so contributors can actually follow the status.

Suggested change
" HNSW algorithm (coordinode-vector crate) is implemented, but not wired to the RPC handler."
" Tracked as gap G007 in coordinode-python GAPS.md."
" HNSW algorithm (coordinode-vector crate) is implemented, but not wired to the RPC handler"
" in the upstream CoordiNode server yet."

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/release.yml (1)

30-38: ⚠️ Potential issue | 🟠 Major

Pin all GitHub Actions in the release workflow to immutable commit SHAs.

This workflow publishes packages to PyPI with id-token: write permissions and repo write access, but every uses: entry references a mutable tag. GitHub's secure-use guidance requires full-length commit SHAs for immutable action references (docs).

The following mutable refs need pinning:

  • Line 30: actions/checkout@v4
  • Line 35: astral-sh/setup-uv@v4
  • Line 51: actions/upload-artifact@v4
  • Line 71: actions/download-artifact@v4
  • Line 76: pypa/gh-action-pypi-publish@release/v1
  • Line 87: actions/download-artifact@v4 (duplicate)
  • Line 93: softprops/action-gh-release@v2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 30 - 38, Replace every mutable
action ref in the release workflow with its corresponding full-length commit
SHA: update the uses entries for actions/checkout@v4, astral-sh/setup-uv@v4,
actions/upload-artifact@v4, actions/download-artifact@v4 (both occurrences),
pypa/gh-action-pypi-publish@release/v1, and softprops/action-gh-release@v2 to
pinned refs like actions/checkout@<full-sha> etc.; fetch the exact SHAs from the
respective GitHub action repositories (tags/releases page) and substitute each
uses: line so the workflow references the immutable commit SHA instead of the
tag.
coordinode/_types.py (1)

36-45: ⚠️ Potential issue | 🟠 Major

Exclude bool from the vector fast-path.

bool is a subclass of int, so lists like [True, False] currently satisfy the numeric-list check on line 40 and get serialized as vector_value. This causes silent type corruption—the values round-trip back as floats [1.0, 0.0] instead of booleans.

Fix by adding and not isinstance(v, bool) to the predicate:

Suggested fix
-        if py_val and all(isinstance(v, int | float) for v in py_val):
+        if py_val and all(isinstance(v, int | float) and not isinstance(v, bool) for v in py_val):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coordinode/_types.py` around lines 36 - 45, The numeric-list fast-path
incorrectly treats booleans as ints (since bool is a subclass of int) causing
bool lists to be serialized as Vector; modify the predicate in the branch that
checks py_val (the all(isinstance(v, int | float) for v in py_val) used before
constructing Vector(values=[...]) and calling pv.vector_value.CopyFrom(vec)) to
exclude bools by adding an explicit not isinstance(v, bool) check (i.e., require
each v to be int or float and not bool); otherwise fall back to creating a
PropertyList via PropertyList(values=[to_property_value(v) for v in py_val]) and
pv.list_value.CopyFrom(pl).
♻️ Duplicate comments (2)
llama-index-coordinode/llama_index/graph_stores/coordinode/base.py (2)

80-83: ⚠️ Potential issue | 🟠 Major

Generate stable Cypher parameter names for property filters.

_cypher_ident() only fixes the left-hand side. Reusing raw keys in ${k} still breaks on valid property names like foo-bar, first name, or x.y, so get(properties=...) can still emit invalid Cypher.

🛠️ Suggested change
         elif properties:
-            where_clauses = " AND ".join(f"n.{_cypher_ident(k)} = ${k}" for k in properties)
-            cypher = f"MATCH (n) WHERE {where_clauses} RETURN n, n.id AS _nid LIMIT 1000"
-            result = self._client.cypher(cypher, params=properties)
+            params: dict[str, Any] = {}
+            clauses: list[str] = []
+            for i, (key, value) in enumerate(properties.items()):
+                param_name = f"p{i}"
+                clauses.append(f"n.{_cypher_ident(key)} = ${param_name}")
+                params[param_name] = value
+            where_clauses = " AND ".join(clauses)
+            cypher = f"MATCH (n) WHERE {where_clauses} RETURN n, n.id AS _nid LIMIT 1000"
+            result = self._client.cypher(cypher, params=params)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 80 - 83, The WHERE clause currently uses raw property keys as Cypher
parameter names (${k}), which breaks for keys with characters like spaces,
dashes, or dots; update the properties branch in the method that builds the
Cypher to generate stable safe parameter names (e.g., map each original key k to
a sanitized param name such as param_<sanitized_k> or an index-based token), use
_cypher_ident(k) for the left-hand side as before, interpolate the sanitized
parameter names in the WHERE clauses (e.g., n.{_cypher_ident(k)} = $<sanitized>)
and pass a new params dict mapping those sanitized names to the original values
when calling self._client.cypher so keys like "foo-bar" or "first name" no
longer produce invalid Cypher.

117-130: ⚠️ Potential issue | 🟠 Major

Use the stored adapter ID consistently in traversal APIs.

get(), upsert_relations(), and delete() all key on n.id, but get_triplets() and get_rel_map() still match/return id(n) / id(m). That makes get_rel_map(get(ids=[...])) miss its seed nodes and produces relation endpoints that won't round-trip back into the rest of this adapter.

🛠️ Suggested change
         cypher = (
             f"MATCH (n)-{rel_pattern}->(m) {where} "
-            "RETURN n, type(r) AS rel_type, m, id(n) AS _src_id, id(m) AS _dst_id "
+            "RETURN n, type(r) AS rel_type, m, n.id AS _src_id, m.id AS _dst_id "
             "LIMIT 1000"
         )
@@
         cypher = (
             f"MATCH (n)-[r*1..{depth}]->(m) "
-            f"WHERE id(n) IN $ids "
-            f"RETURN n, r, m, id(n) AS _src_id, id(m) AS _dst_id "
+            f"WHERE n.id IN $ids "
+            f"RETURN n, r, m, n.id AS _src_id, m.id AS _dst_id "
             f"LIMIT {limit}"
         )

Also applies to: 147-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 117 - 130, get_triplets() and get_rel_map() are returning id(n)/id(m)
(Neo4j internal ids) while other APIs (get, upsert_relations, delete) use the
stored adapter node id property, causing mismatched seeds; change the code in
get_triplets() and get_rel_map() to extract and return the node's stored adapter
id (the same property used by get/upsert_relations/delete) instead of
id(n)/id(m). Locate the occurrences that build _src_id/_dst_id (e.g., the "id(n)
AS _src_id, id(m) AS _dst_id" cypher and the subsequent src_id/dst_id
assignments in get_triplets() and get_rel_map()) and replace them to read the
node property (e.g., n.id or the property key the adapter uses) from the row
data (src_data/dst_data) or via _node_result_to_labelled so returned relation
endpoints match the adapter's stored IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release-please.yml:
- Around line 16-20: The workflow is referencing the mutable tag
googleapis/release-please-action@v4 (id: rp, with: config-file:
release-please-config.json, manifest-file: .release-please-manifest.json);
replace that mutable tag with the action's full commit SHA (use the full-length
commit hash from the googleapis/release-please-action repository at the desired
v4 release) so the uses: line becomes an immutable reference to that commit SHA.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 150-169: The post-query filtering of ignored relationship types
(using the rels / ignored logic after fetching result) happens after the Cypher
LIMIT, so filtered-out paths reduce returned triplets; fix by moving the filter
into the Cypher query: change the constructed cypher (the cypher variable) to
match paths (e.g., MATCH p=(n)-[r*1..{depth}]->(m)) and add a WHERE clause using
relationships(p) to ensure ALL(rel IN relationships(p) WHERE type(rel) NOT IN
$ignored) (and pass ignored as a param) so unwanted relationship types are
excluded before LIMIT; update references that call self._client.cypher
(params={"ids": node_ids}) to include the ignored param and remove the
post-query ignored check on rels.

In `@Makefile`:
- Around line 30-38: The install and install-pip Makefile targets run the proto
target before dependencies (grpc_tools.protoc) are available; update the target
dependencies so the environment is populated first by running uv sync / pip
installs before invoking proto — for example, remove proto as a prerequisite
from the install and install-pip targets and ensure uv sync (for install) or the
pip-install steps (for install-pip) run before calling proto, or make proto
depend on a new "deps" prerequisite that runs uv sync or pip installs; adjust
the install, install-pip, and proto targets accordingly so proto is only
executed after dependencies are installed.

In `@tests/integration/test_basic.py`:
- Around line 10-12: Replace the shared constant fixture identifiers used across
tests (the constant `name`/`id` and any global MATCH/cleanup usages) with
per-test unique IDs: generate a UUID or timestamp-based id inside each test or a
pytest fixture (e.g., a function-scoped fixture) and use that value for
creating, MATCH-ing, and cleaning up rows so each test only touches its own
fixture; update the tests referenced around the MATCH and cleanup logic to
accept the new fixture or local `test_id` variable and ensure cleanup uses that
same `test_id` rather than the shared constant.
- Around line 62-69: The xfail marker currently uses pytest.mark.xfail(...,
strict=False) which permits the test to pass silently once the vector RPC starts
working; update the marker to strict=True so the test will fail if the
previously-expected failure no longer occurs—locate the pytest.mark.xfail
decorator in tests/integration/test_basic.py and change the strict parameter
from False to True while keeping the existing reason text unchanged.

---

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 30-38: Replace every mutable action ref in the release workflow
with its corresponding full-length commit SHA: update the uses entries for
actions/checkout@v4, astral-sh/setup-uv@v4, actions/upload-artifact@v4,
actions/download-artifact@v4 (both occurrences),
pypa/gh-action-pypi-publish@release/v1, and softprops/action-gh-release@v2 to
pinned refs like actions/checkout@<full-sha> etc.; fetch the exact SHAs from the
respective GitHub action repositories (tags/releases page) and substitute each
uses: line so the workflow references the immutable commit SHA instead of the
tag.

In `@coordinode/_types.py`:
- Around line 36-45: The numeric-list fast-path incorrectly treats booleans as
ints (since bool is a subclass of int) causing bool lists to be serialized as
Vector; modify the predicate in the branch that checks py_val (the
all(isinstance(v, int | float) for v in py_val) used before constructing
Vector(values=[...]) and calling pv.vector_value.CopyFrom(vec)) to exclude bools
by adding an explicit not isinstance(v, bool) check (i.e., require each v to be
int or float and not bool); otherwise fall back to creating a PropertyList via
PropertyList(values=[to_property_value(v) for v in py_val]) and
pv.list_value.CopyFrom(pl).

---

Duplicate comments:
In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py`:
- Around line 80-83: The WHERE clause currently uses raw property keys as Cypher
parameter names (${k}), which breaks for keys with characters like spaces,
dashes, or dots; update the properties branch in the method that builds the
Cypher to generate stable safe parameter names (e.g., map each original key k to
a sanitized param name such as param_<sanitized_k> or an index-based token), use
_cypher_ident(k) for the left-hand side as before, interpolate the sanitized
parameter names in the WHERE clauses (e.g., n.{_cypher_ident(k)} = $<sanitized>)
and pass a new params dict mapping those sanitized names to the original values
when calling self._client.cypher so keys like "foo-bar" or "first name" no
longer produce invalid Cypher.
- Around line 117-130: get_triplets() and get_rel_map() are returning
id(n)/id(m) (Neo4j internal ids) while other APIs (get, upsert_relations,
delete) use the stored adapter node id property, causing mismatched seeds;
change the code in get_triplets() and get_rel_map() to extract and return the
node's stored adapter id (the same property used by get/upsert_relations/delete)
instead of id(n)/id(m). Locate the occurrences that build _src_id/_dst_id (e.g.,
the "id(n) AS _src_id, id(m) AS _dst_id" cypher and the subsequent src_id/dst_id
assignments in get_triplets() and get_rel_map()) and replace them to read the
node property (e.g., n.id or the property key the adapter uses) from the row
data (src_data/dst_data) or via _node_result_to_labelled so returned relation
endpoints match the adapter's stored IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad4b036d-6e0f-4ca6-bdca-d997bad323a6

📥 Commits

Reviewing files that changed from the base of the PR and between bc156c3 and ea8f1ff.

📒 Files selected for processing (9)
  • .github/workflows/release-please.yml
  • .github/workflows/release.yml
  • Makefile
  • coordinode/__init__.py
  • coordinode/_types.py
  • coordinode/client.py
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
  • release-please-config.json
  • tests/integration/test_basic.py

Comment on lines +16 to +20
- uses: googleapis/release-please-action@v4
id: rp
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
python - <<'PY'
import pathlib, re

path = pathlib.Path(".github/workflows/release-please.yml")
for i, line in enumerate(path.read_text().splitlines(), 1):
    m = re.search(r'uses:\s+(\S+)@([^\s#]+)', line)
    if m and not re.fullmatch(r'[0-9a-fA-F]{40}', m.group(2)):
        print(f"{path}:{i}: {m.group(1)}@{m.group(2)}")
PY

Repository: structured-world/coordinode-python

Length of output: 154


Pin the release-please action to an immutable commit SHA.

This workflow runs on main with write permissions, but googleapis/release-please-action@v4 uses a mutable tag. GitHub's secure-use guidance recommends full-length commit SHAs as the only immutable action references. (docs.github.com)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-please.yml around lines 16 - 20, The workflow is
referencing the mutable tag googleapis/release-please-action@v4 (id: rp, with:
config-file: release-please-config.json, manifest-file:
.release-please-manifest.json); replace that mutable tag with the action's full
commit SHA (use the full-length commit hash from the
googleapis/release-please-action repository at the desired v4 release) so the
uses: line becomes an immutable reference to that commit SHA.

Comment on lines 150 to +169
cypher = (
f"MATCH (n)-[r*1..{depth}]->(m) "
f"WHERE id(n) IN $ids "
f"RETURN n, r, m, id(n) AS _src_id, id(m) AS _dst_id "
f"LIMIT {limit}"
)
result = self._client.cypher(cypher, params={"ids": ids})

triplets: List[List[LabelledNode]] = []
for row in result.rows:
src_data, rels, dst_data, src_id, dst_id = (
row[0], row[1], row[2], str(row[3]), str(row[4])
)
result = self._client.cypher(cypher, params={"ids": node_ids})

triplets: list[list[LabelledNode]] = []
for row in result:
src_data = row.get("n", {})
dst_data = row.get("m", {})
src_id = str(row.get("_src_id", ""))
dst_id = str(row.get("_dst_id", ""))
# Variable-length path [r*1..N] returns a list of relationship dicts.
rels = row.get("r", [])
# Skip paths that contain any ignored relationship type.
if ignored and isinstance(rels, list):
if any(isinstance(r, dict) and r.get("type") in ignored for r in rels):
continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "base.py" | grep coordinode

Repository: structured-world/coordinode-python

Length of output: 146


🏁 Script executed:

wc -l ./llama-index-coordinode/llama_index/graph_stores/coordinode/base.py

Repository: structured-world/coordinode-python

Length of output: 150


🏁 Script executed:

sed -n '130,190p' ./llama-index-coordinode/llama_index/graph_stores/coordinode/base.py | cat -n

Repository: structured-world/coordinode-python

Length of output: 3023


🏁 Script executed:

sed -n '137,185p' ./llama-index-coordinode/llama_index/graph_stores/coordinode/base.py | cat -n

Repository: structured-world/coordinode-python

Length of output: 2431


🏁 Script executed:

grep -r "get_rel_map" ./llama-index-coordinode --include="*.py" -B 2 -A 5

Repository: structured-world/coordinode-python

Length of output: 825


🏁 Script executed:

grep -r "get_rel_map\|ignore_rels" ./llama-index-coordinode --include="*.py" -l

Repository: structured-world/coordinode-python

Length of output: 146


Filter ignored relationship types before LIMIT.

The ignore_rels filtering happens after the Cypher LIMIT, causing fewer results than expected. When a graph has paths dominated by ignored relationship types, the method can return far fewer than limit triplets even when more eligible paths exist. Either add a WHERE clause to the Cypher query to filter relationship types, or over-fetch results until the post-filtered set reaches the requested limit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@llama-index-coordinode/llama_index/graph_stores/coordinode/base.py` around
lines 150 - 169, The post-query filtering of ignored relationship types (using
the rels / ignored logic after fetching result) happens after the Cypher LIMIT,
so filtered-out paths reduce returned triplets; fix by moving the filter into
the Cypher query: change the constructed cypher (the cypher variable) to match
paths (e.g., MATCH p=(n)-[r*1..{depth}]->(m)) and add a WHERE clause using
relationships(p) to ensure ALL(rel IN relationships(p) WHERE type(rel) NOT IN
$ignored) (and pass ignored as a param) so unwanted relationship types are
excluded before LIMIT; update references that call self._client.cypher
(params={"ids": node_ids}) to include the ignored param and remove the
post-query ignored check on rels.

Comment on lines +30 to 38
# Install using uv (recommended for contributors)
install: proto
uv sync

# Install using pip (alternative — works without uv)
install-pip: proto
pip install -e "coordinode[dev]"
pip install -e langchain-coordinode/
pip install -e llama-index-coordinode/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Install dependencies before generating stubs.

install and install-pip currently invoke proto before the environment contains grpc_tools.protoc, so both targets can fail on a clean checkout. This also disagrees with CI, which already does uv sync before uv run make proto.

🔧 Suggested fix
 # Install using uv (recommended for contributors)
-install: proto
-	uv sync
+install:
+	uv sync
+	uv run $(MAKE) proto
 
 # Install using pip (alternative — works without uv)
-install-pip: proto
+install-pip:
 	pip install -e "coordinode[dev]"
+	$(MAKE) proto
 	pip install -e langchain-coordinode/
 	pip install -e llama-index-coordinode/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Install using uv (recommended for contributors)
install: proto
uv sync
# Install using pip (alternative — works without uv)
install-pip: proto
pip install -e "coordinode[dev]"
pip install -e langchain-coordinode/
pip install -e llama-index-coordinode/
# Install using uv (recommended for contributors)
install:
uv sync
uv run $(MAKE) proto
# Install using pip (alternative — works without uv)
install-pip:
pip install -e "coordinode[dev]"
$(MAKE) proto
pip install -e langchain-coordinode/
pip install -e llama-index-coordinode/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 30 - 38, The install and install-pip Makefile targets
run the proto target before dependencies (grpc_tools.protoc) are available;
update the target dependencies so the environment is populated first by running
uv sync / pip installs before invoking proto — for example, remove proto as a
prerequisite from the install and install-pip targets and ensure uv sync (for
install) or the pip-install steps (for install-pip) run before calling proto, or
make proto depend on a new "deps" prerequisite that runs uv sync or pip
installs; adjust the install, install-pip, and proto targets accordingly so
proto is only executed after dependencies are installed.

Comment on lines 10 to 12
import os

import pytest
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use per-test fixture IDs instead of shared constants.

These tests create and delete rows by constant name/id values. If a previous run crashes or two jobs point at the same instance, the MATCH and cleanup can observe or delete another run’s fixture instead of this test’s.

🔧 Suggested fix
 import os
+from uuid import uuid4
 
 import pytest
@@
 def test_create_and_get_node(client):
+    name = f"sdk-test-node-{uuid4().hex}"
     result = client.cypher(
         "CREATE (n:IntegrationTest {name: $name}) RETURN n.name AS name",
-        params={"name": "sdk-test-node"},
+        params={"name": name},
     )
     assert result, "CREATE returned no rows"
-    assert result[0]["name"] == "sdk-test-node"
+    assert result[0]["name"] == name
@@
     found = client.cypher(
         "MATCH (n:IntegrationTest {name: $name}) RETURN n.name AS name",
-        params={"name": "sdk-test-node"},
+        params={"name": name},
     )
     assert found, "MATCH returned no rows"
-    assert found[0]["name"] == "sdk-test-node"
+    assert found[0]["name"] == name
@@
     client.cypher(
         "MATCH (n:IntegrationTest {name: $name}) DELETE n",
-        params={"name": "sdk-test-node"},
+        params={"name": name},
     )
@@
 def test_vector_search(client):
@@
+    doc_id = f"vs-test-{uuid4().hex}"
     vec = [0.1] * 16
     client.cypher(
-        "CREATE (d:VecTestDoc {id: 'vs-test', embedding: $vec})",
-        params={"vec": vec},
+        "CREATE (d:VecTestDoc {id: $id, embedding: $vec})",
+        params={"id": doc_id, "vec": vec},
     )
@@
     finally:
-        client.cypher("MATCH (d:VecTestDoc {id: 'vs-test'}) DELETE d")
+        client.cypher(
+            "MATCH (d:VecTestDoc {id: $id}) DELETE d",
+            params={"id": doc_id},
+        )

Also applies to: 37-59, 70-92

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_basic.py` around lines 10 - 12, Replace the shared
constant fixture identifiers used across tests (the constant `name`/`id` and any
global MATCH/cleanup usages) with per-test unique IDs: generate a UUID or
timestamp-based id inside each test or a pytest fixture (e.g., a function-scoped
fixture) and use that value for creating, MATCH-ing, and cleaning up rows so
each test only touches its own fixture; update the tests referenced around the
MATCH and cleanup logic to accept the new fixture or local `test_id` variable
and ensure cleanup uses that same `test_id` rather than the shared constant.

Comment on lines +62 to +69
@pytest.mark.xfail(
reason=(
"VectorServiceImpl is a stub in server/src/services/vector.rs — always returns []."
" HNSW algorithm (coordinode-vector crate) is implemented, but not wired to the RPC handler."
" Tracked as gap G007 in coordinode-python GAPS.md."
),
strict=False,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Make the xfail strict.

With strict=False, this stays green even after the vector RPC starts working, so the expected-failure marker can linger indefinitely.

🔧 Suggested fix
-    strict=False,
+    strict=True,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_basic.py` around lines 62 - 69, The xfail marker
currently uses pytest.mark.xfail(..., strict=False) which permits the test to
pass silently once the vector RPC starts working; update the marker to
strict=True so the test will fail if the previously-expected failure no longer
occurs—locate the pytest.mark.xfail decorator in tests/integration/test_basic.py
and change the strict parameter from False to True while keeping the existing
reason text unchanged.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
coordinode/__init__.py (1)

8-11: ⚠️ Potential issue | 🟡 Minor

Docstring example is inconsistent with the new cypher() return type.

The module docstring shows result.rows but cypher() now returns list[dict[str, Any]] directly. This will cause an AttributeError when users copy the example.

📝 Proposed fix
     with CoordinodeClient("localhost:7080") as db:
         result = db.cypher("MATCH (n:Concept) RETURN n LIMIT 5")
-        for row in result.rows:
+        for row in result:
             print(row)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coordinode/__init__.py` around lines 8 - 11, The docstring example is
outdated because CoordinodeClient.cypher now returns list[dict[str, Any]]
instead of an object with .rows; update the example in the module docstring to
call result = db.cypher(...) and iterate directly (for row in result:
print(row)) or rename to rows = db.cypher(...) and iterate over rows,
referencing CoordinodeClient.cypher and the result variable so users don't get
an AttributeError.
♻️ Duplicate comments (2)
.github/workflows/release-please.yml (1)

16-20: ⚠️ Potential issue | 🟠 Major

Pin the release-please action to an immutable commit SHA.

This workflow runs with write permissions on main, but googleapis/release-please-action@v4 uses a mutable tag. Per GitHub's security hardening guidance, full-length commit SHAs are the only immutable action references.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-please.yml around lines 16 - 20, The workflow uses
a mutable tag "uses: googleapis/release-please-action@v4" (id: rp) which must be
pinned to an immutable full-length commit SHA; update that line to reference the
action by its commit SHA (e.g. "uses:
googleapis/release-please-action@<full-commit-sha>") while keeping the existing
inputs (config-file: release-please-config.json and manifest-file:
.release-please-manifest.json) unchanged so the workflow behavior stays the
same.
Makefile (1)

30-38: ⚠️ Potential issue | 🟠 Major

Install dependencies before generating proto stubs.

Both install and install-pip targets depend on proto, but proto requires grpc_tools.protoc which isn't available until after uv sync or pip install. On a clean checkout, make install will fail because python3 -m grpc_tools.protoc cannot find the module.

CI works because it runs uv sync before uv run make proto separately. The Makefile targets should follow the same pattern.

🔧 Proposed fix
 # Install using uv (recommended for contributors)
-install: proto
-	uv sync
+install:
+	uv sync
+	uv run $(MAKE) proto

 # Install using pip (alternative — works without uv)
-install-pip: proto
+install-pip:
 	pip install -e "coordinode[dev]"
+	$(MAKE) proto
 	pip install -e langchain-coordinode/
 	pip install -e llama-index-coordinode/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 30 - 38, The Makefile currently lists proto as a
prerequisite for the install and install-pip targets but proto requires
grpc_tools.protoc which is only available after dependencies are installed;
change the order so the dependency install steps run first and proto is invoked
afterwards: update the install target to run uv sync (the dependency install)
before calling the proto generation, and update install-pip to run the pip
install commands before invoking the proto target (or remove proto as a
prerequisite and invoke it as a recipe step after installs); adjust references
to the install, install-pip and proto targets accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 35-37: The workflow pins use mutable action tags (e.g.,
astral-sh/setup-uv@v4, actions/checkout, actions/upload-artifact,
actions/download-artifact, pypa/gh-action-pypi-publish,
softprops/action-gh-release); replace each mutable ref with the corresponding
full-length commit SHA for the release of that action (locate the specific
commit SHA for the desired version on the action's repo and replace the tag with
that SHA) and update every occurrence in the file so all actions are pinned to
immutable commits.

---

Outside diff comments:
In `@coordinode/__init__.py`:
- Around line 8-11: The docstring example is outdated because
CoordinodeClient.cypher now returns list[dict[str, Any]] instead of an object
with .rows; update the example in the module docstring to call result =
db.cypher(...) and iterate directly (for row in result: print(row)) or rename to
rows = db.cypher(...) and iterate over rows, referencing CoordinodeClient.cypher
and the result variable so users don't get an AttributeError.

---

Duplicate comments:
In @.github/workflows/release-please.yml:
- Around line 16-20: The workflow uses a mutable tag "uses:
googleapis/release-please-action@v4" (id: rp) which must be pinned to an
immutable full-length commit SHA; update that line to reference the action by
its commit SHA (e.g. "uses: googleapis/release-please-action@<full-commit-sha>")
while keeping the existing inputs (config-file: release-please-config.json and
manifest-file: .release-please-manifest.json) unchanged so the workflow behavior
stays the same.

In `@Makefile`:
- Around line 30-38: The Makefile currently lists proto as a prerequisite for
the install and install-pip targets but proto requires grpc_tools.protoc which
is only available after dependencies are installed; change the order so the
dependency install steps run first and proto is invoked afterwards: update the
install target to run uv sync (the dependency install) before calling the proto
generation, and update install-pip to run the pip install commands before
invoking the proto target (or remove proto as a prerequisite and invoke it as a
recipe step after installs); adjust references to the install, install-pip and
proto targets accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af84553c-a76d-4710-8612-cea30934d226

📥 Commits

Reviewing files that changed from the base of the PR and between bc156c3 and ea8f1ff.

📒 Files selected for processing (9)
  • .github/workflows/release-please.yml
  • .github/workflows/release.yml
  • Makefile
  • coordinode/__init__.py
  • coordinode/_types.py
  • coordinode/client.py
  • llama-index-coordinode/llama_index/graph_stores/coordinode/base.py
  • release-please-config.json
  • tests/integration/test_basic.py

Comment on lines +35 to 37
- uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check all action references in release.yml for mutable tags
grep -nE 'uses:\s+\S+@' .github/workflows/release.yml | grep -vE '@[0-9a-f]{40}'

Repository: structured-world/coordinode-python

Length of output: 393


Pin GitHub Actions to immutable commit SHAs.

astral-sh/setup-uv@v4 uses a mutable tag. For workflows with write permissions, GitHub recommends pinning actions to full-length commit SHAs. This pattern occurs throughout the file—actions/checkout, actions/upload-artifact, actions/download-artifact, pypa/gh-action-pypi-publish, and softprops/action-gh-release are also unpinned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release.yml around lines 35 - 37, The workflow pins use
mutable action tags (e.g., astral-sh/setup-uv@v4, actions/checkout,
actions/upload-artifact, actions/download-artifact, pypa/gh-action-pypi-publish,
softprops/action-gh-release); replace each mutable ref with the corresponding
full-length commit SHA for the release of that action (locate the specific
commit SHA for the desired version on the action's repo and replace the tag with
that SHA) and update every occurrence in the file so all actions are pinned to
immutable commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: SDK API correctness — constructor, cypher() result, VectorResult fields

2 participants