Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Changelog - [0.2.0] - yyyy-mm-dd

### New Features
1. Project all (non-string) node properties using the appropriate (integer/float) types.


### Bug Fixes
1. Fix GDS call for path algorithms and clean up returned data format.


### Other Changes
1. Removed minimum_weight_k_spanning_tree since it is write mode only, which may modify the database unexpectedly.
100 changes: 81 additions & 19 deletions mcp_server/src/mcp_server_neo4j_gds/gds.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,95 @@ def projected_graph(gds, undirected=False):
"""
graph_name = f"temp_graph_{uuid.uuid4().hex[:8]}"
try:
# Get relationship properties (non-string)
rel_properties = gds.run_cypher(
"MATCH (n)-[r]-(m) RETURN DISTINCT keys(properties(r))"
)["keys(properties(r))"][0]
# Include all properties that are not STRING
valid_properties = {}
valid_rel_properties = {}
for i in range(len(rel_properties)):
pi = gds.run_cypher(
f"MATCH (n)-[r]-(m) RETURN distinct r.{rel_properties[i]} IS :: STRING AS ISSTRING"
)
if pi.shape[0] == 1 and bool(pi["ISSTRING"][0]) is False:
valid_properties[rel_properties[i]] = f"r.{rel_properties[i]}"
prop_map = ", ".join(f"{prop}: r.{prop}" for prop in valid_properties)
valid_rel_properties[rel_properties[i]] = f"r.{rel_properties[i]}"
rel_prop_map = ", ".join(f"{prop}: r.{prop}" for prop in valid_rel_properties)

# Get node properties (non-string, compatible with GDS)
node_properties = gds.run_cypher(
"MATCH (n) RETURN DISTINCT keys(properties(n))"
)["keys(properties(n))"][0]
valid_node_properties = {}
for i in range(len(node_properties)):
# Check property types and whether all values are whole numbers
type_check = gds.run_cypher(
f"""
MATCH (n)
WHERE n.{node_properties[i]} IS NOT NULL
WITH n.{node_properties[i]} AS prop
RETURN
prop IS :: STRING AS ISSTRING,
CASE
WHEN prop IS :: STRING THEN null
ELSE prop % 1 = 0
END AS IS_WHOLE_NUMBER
LIMIT 10
"""
)

if not type_check.empty:
# Check if any value is a string - if so, skip this property
has_strings = any(type_check["ISSTRING"])

if not has_strings:
# All values are numeric, check if all are whole numbers
whole_numbers = type_check["IS_WHOLE_NUMBER"].dropna()
if len(whole_numbers) > 0 and all(whole_numbers):
# All values are whole numbers - use as integer
valid_node_properties[node_properties[i]] = (
f"n.{node_properties[i]}"
)
else:
# Has decimal values - use as float
valid_node_properties[node_properties[i]] = (
f"toFloat(n.{node_properties[i]})"
)

node_prop_map = ", ".join(
f"{prop}: {expr}" for prop, expr in valid_node_properties.items()
)
logger.info(f"Node property map: '{node_prop_map}'")
# Configure graph projection based on undirected parameter
# Create data configuration (node/relationship structure)
data_config_parts = [
"sourceNodeLabels: labels(n)",
"targetNodeLabels: labels(m)",
"relationshipType: type(r)",
]

if node_prop_map:
data_config_parts.extend(
[
f"sourceNodeProperties: {{{node_prop_map}}}",
f"targetNodeProperties: {{{node_prop_map}}}",
]
)

if rel_prop_map:
data_config_parts.append(f"relationshipProperties: {{{rel_prop_map}}}")

data_config = ", ".join(data_config_parts)

# Create additional configuration
additional_config_parts = []
if undirected:
# For undirected graphs, use undirectedRelationshipTypes: ['*'] to make all relationships undirected
additional_config_parts.append("undirectedRelationshipTypes: ['*']")

additional_config = (
", ".join(additional_config_parts) if additional_config_parts else ""
)

# Use separate data and additional configuration parameters
if additional_config:
G, _ = gds.graph.cypher.project(
f"""
MATCH (n)-[r]-(m)
Expand All @@ -80,19 +153,13 @@ def projected_graph(gds, undirected=False):
$graph_name,
n,
m,
{{
sourceNodeLabels: labels(n),
targetNodeLabels: labels(m),
relationshipType: type(r),
relationshipProperties: {{{prop_map}}},
undirectedRelationshipTypes: ['*']
}}
{{{data_config}}},
{{{additional_config}}}
)
""",
graph_name=graph_name,
)
else:
# Default directed projection
G, _ = gds.graph.cypher.project(
f"""
MATCH (n)-[r]-(m)
Expand All @@ -101,12 +168,7 @@ def projected_graph(gds, undirected=False):
$graph_name,
n,
m,
{{
sourceNodeLabels: labels(n),
targetNodeLabels: labels(m),
relationshipType: type(r),
relationshipProperties: {{{prop_map}}}
}}
{{{data_config}}}
)
""",
graph_name=graph_name,
Expand Down
Loading