From 213f78e8aee109fbcb0a0ad4d48938b9ecd18d6c Mon Sep 17 00:00:00 2001 From: Dmitrii Kataraev Date: Thu, 11 Jun 2026 16:33:39 -0700 Subject: [PATCH 1/6] feat(tool_falkordb): query FalkorDB graphs with Cypher as an agent tool New tool node exposing query, list_graphs and get_schema. Queries run read-only via GRAPH.RO_QUERY (the server itself rejects write clauses) unless allow_writes is enabled in config; values go through Cypher parameters so user data is never spliced into query text. Rows are capped at a configurable max with a truncated flag; nodes/edges/paths serialize to plain JSON; redis errors come back as error dicts. memory_persistent moves to redis>=7.1,<8: falkordb requires redis>=7.1 (older falkordb requires <6), so the previous >=6.4,<7 pin made the engine-wide uv constraints solve unsatisfiable. The redis-py surface memory_persistent uses (get/exists/hget/incrby/lrange/pexpire/pipeline/ pttl/rpush/sadd/scard/sismember/smembers/srem) is identical across 6.x and 7.x. Full-tree solve verified with uv pip compile: redis==7.4.1, falkordb==1.6.1. Co-Authored-By: Claude Fable 5 --- .../nodes/memory_persistent/requirements.txt | 6 +- nodes/src/nodes/tool_falkordb/IGlobal.py | 112 +++++++ nodes/src/nodes/tool_falkordb/IInstance.py | 265 +++++++++++++++++ nodes/src/nodes/tool_falkordb/README.md | 58 ++++ nodes/src/nodes/tool_falkordb/__init__.py | 35 +++ nodes/src/nodes/tool_falkordb/doc.md | 88 ++++++ nodes/src/nodes/tool_falkordb/falkordb.svg | 7 + .../src/nodes/tool_falkordb/requirements.txt | 1 + nodes/src/nodes/tool_falkordb/services.json | 108 +++++++ nodes/test/mocks/falkordb/__init__.py | 71 +++++ nodes/test/test_tool_falkordb.py | 281 ++++++++++++++++++ 11 files changed, 1031 insertions(+), 1 deletion(-) create mode 100644 nodes/src/nodes/tool_falkordb/IGlobal.py create mode 100644 nodes/src/nodes/tool_falkordb/IInstance.py create mode 100644 nodes/src/nodes/tool_falkordb/README.md create mode 100644 nodes/src/nodes/tool_falkordb/__init__.py create mode 100644 nodes/src/nodes/tool_falkordb/doc.md create mode 100644 nodes/src/nodes/tool_falkordb/falkordb.svg create mode 100644 nodes/src/nodes/tool_falkordb/requirements.txt create mode 100644 nodes/src/nodes/tool_falkordb/services.json create mode 100644 nodes/test/mocks/falkordb/__init__.py create mode 100644 nodes/test/test_tool_falkordb.py diff --git a/nodes/src/nodes/memory_persistent/requirements.txt b/nodes/src/nodes/memory_persistent/requirements.txt index d9f7234c5..662854fc8 100644 --- a/nodes/src/nodes/memory_persistent/requirements.txt +++ b/nodes/src/nodes/memory_persistent/requirements.txt @@ -1 +1,5 @@ -redis>=6.4.0,<7.0.0 +# falkordb (any version) needs redis>=7.1 or <6; the previous >=6.4,<7 pin made +# the engine-wide constraints solve unsatisfiable. The redis-py surface this +# node uses (get/exists/hget/incrby/lrange/pexpire/pipeline/pttl/rpush/sadd/ +# scard/sismember/smembers/srem) is identical across 6.x and 7.x. +redis>=7.1,<8 diff --git a/nodes/src/nodes/tool_falkordb/IGlobal.py b/nodes/src/nodes/tool_falkordb/IGlobal.py new file mode 100644 index 000000000..747b7a0e4 --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/IGlobal.py @@ -0,0 +1,112 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +""" +FalkorDB tool node - global (shared) state. + +Reads the connection settings from config, creates a FalkorDB client and +verifies connectivity. Tool logic lives on IInstance via @tool_function. +""" + +from __future__ import annotations + +from ai.common.config import Config +from rocketlib import IGlobalBase, OPEN_MODE, debug, warning + +from falkordb import FalkorDB + + +def _int_or(value, default: int, *, lo: int, hi: int) -> int: + try: + n = int(value) + except (TypeError, ValueError): + return default + return max(lo, min(n, hi)) + + +def _bool_of(value) -> bool: + if isinstance(value, str): + return value.strip().lower() in {'1', 'true', 'yes', 'on'} + return bool(value) + + +class IGlobal(IGlobalBase): + """Global state for tool_falkordb.""" + + client: FalkorDB | None = None + # Unprefixed config values set during beginGlobal. + graph_name: str = 'agent' + allow_writes: bool = False + max_rows: int = 250 + query_timeout_ms: int = 30000 + + def beginGlobal(self) -> None: + if self.IEndpoint.endpoint.openMode == OPEN_MODE.CONFIG: + return + + cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) + + host = str((cfg.get('host') or 'localhost')).strip() or 'localhost' + port = _int_or(cfg.get('port'), 6379, lo=1, hi=65535) + username = str((cfg.get('username') or '')).strip() + # Do NOT strip the password — whitespace is valid in passwords. + password = str(cfg.get('password') or '') + tls = _bool_of(cfg.get('tls')) + + self.graph_name = str((cfg.get('graph') or 'agent')).strip() or 'agent' + self.allow_writes = _bool_of(cfg.get('allow_writes')) + self.max_rows = _int_or(cfg.get('max_rows'), 250, lo=1, hi=25000) + self.query_timeout_ms = _int_or(cfg.get('query_timeout_ms'), 30000, lo=100, hi=600000) + + client_kwargs = {'host': host, 'port': port} + if username: + client_kwargs['username'] = username + if password: + client_kwargs['password'] = password + if tls: + client_kwargs['ssl'] = True + + self.client = FalkorDB(**client_kwargs) + # Fail fast on bad host/credentials instead of on the first tool call. + self.client.list_graphs() + debug(f'tool_falkordb: connected to {host}:{port}, default graph={self.graph_name}') + + def validateConfig(self) -> None: + try: + cfg = Config.getNodeConfig(self.glb.logicalType, self.glb.connConfig) + host = str((cfg.get('host') or '')).strip() + if not host: + warning('host is required') + except Exception as e: + warning(str(e)) + + def endGlobal(self) -> None: + if self.client is not None: + try: + self.client.close() + except Exception as e: + warning(f'tool_falkordb: close failed: {e}') + finally: + self.client = None diff --git a/nodes/src/nodes/tool_falkordb/IInstance.py b/nodes/src/nodes/tool_falkordb/IInstance.py new file mode 100644 index 000000000..6652ad2c8 --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/IInstance.py @@ -0,0 +1,265 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +""" +FalkorDB tool node instance. + +Exposes ``query`` (run Cypher against a graph), ``list_graphs`` and +``get_schema`` as agent tools. Queries run read-only via GRAPH.RO_QUERY +(write rejection enforced server-side) unless writes are enabled in config. +""" + +from __future__ import annotations + +import datetime + +from redis.exceptions import RedisError + +from rocketlib import IInstanceBase, tool_function + +from ai.common.utils import normalize_tool_input + +from .IGlobal import IGlobal + + +def _serialize_value(value): + """Convert FalkorDB result cells (Node/Edge/Path/temporals) to plain JSON-safe data.""" + # Graph entities are duck-typed: falkordb.Node has labels+properties, + # Edge has relation+src_node/dest_node, Path has nodes()/edges(). + if hasattr(value, 'labels') and hasattr(value, 'properties'): + return { + 'id': getattr(value, 'id', None), + 'labels': list(value.labels or []), + 'properties': _serialize_value(value.properties), + } + if hasattr(value, 'relation') and hasattr(value, 'properties'): + return { + 'id': getattr(value, 'id', None), + 'type': value.relation, + 'src': getattr(value, 'src_node', None), + 'dst': getattr(value, 'dest_node', None), + 'properties': _serialize_value(value.properties), + } + if hasattr(value, 'nodes') and hasattr(value, 'edges') and callable(getattr(value, 'nodes', None)): + return { + 'nodes': [_serialize_value(n) for n in value.nodes()], + 'edges': [_serialize_value(e) for e in value.edges()], + } + if isinstance(value, dict): + return {str(k): _serialize_value(v) for k, v in value.items()} + if isinstance(value, (list, tuple)): + return [_serialize_value(v) for v in value] + if isinstance(value, (datetime.datetime, datetime.date, datetime.time)): + return value.isoformat() + if isinstance(value, (str, int, float, bool)) or value is None: + return value + return str(value) + + +def _header_names(header) -> list: + """Extract column names from a QueryResult header ([type, name] pairs or strings).""" + names = [] + for entry in header or []: + if isinstance(entry, (list, tuple)) and len(entry) >= 2: + names.append(str(entry[1])) + else: + names.append(str(entry)) + return names + + +def _write_stats(result) -> dict: + """Collect non-zero write counters from a QueryResult.""" + stats = {} + for attr in ( + 'nodes_created', + 'nodes_deleted', + 'relationships_created', + 'relationships_deleted', + 'properties_set', + 'properties_removed', + 'labels_added', + 'indices_created', + ): + try: + value = getattr(result, attr, 0) or 0 + except (RedisError, ValueError, TypeError): + value = 0 + if value: + stats[attr] = value + return stats + + +class IInstance(IInstanceBase): + IGlobal: IGlobal + + def _select_graph(self, args): + """Resolve the target graph from args or config default.""" + graph_name = args.get('graph') + if graph_name is not None and (not isinstance(graph_name, str) or not graph_name.strip()): + raise ValueError('"graph" must be a non-empty string when provided') + graph_name = (graph_name or self.IGlobal.graph_name).strip() + return self.IGlobal.client.select_graph(graph_name), graph_name + + @tool_function( + input_schema={ + 'type': 'object', + 'required': ['cypher'], + 'properties': { + 'cypher': { + 'type': 'string', + 'description': 'Cypher query to execute. Use $name placeholders with "params" for values — never inline user data into the query string.', + }, + 'params': { + 'type': 'object', + 'description': 'Parameter values referenced as $name in the query (injection-safe).', + }, + 'graph': { + 'type': 'string', + 'description': 'Graph to query; defaults to the graph configured on the node.', + }, + }, + }, + output_schema={ + 'type': 'object', + 'properties': { + 'columns': {'type': 'array', 'items': {'type': 'string'}}, + 'rows': { + 'type': 'array', + 'items': {'type': 'array'}, + 'description': 'Result rows; nodes/edges are serialized to objects.', + }, + 'row_count': {'type': 'integer'}, + 'truncated': {'type': 'boolean', 'description': 'True if rows were cut at the configured cap.'}, + 'stats': { + 'type': 'object', + 'description': 'Write counters (only when writes are enabled and occurred).', + }, + 'error': {'type': 'string', 'description': 'Error message if the query failed.'}, + }, + }, + description=lambda self: ( + f'Run a Cypher query against the FalkorDB graph database ' + f'(default graph: "{self.IGlobal.graph_name}"). ' + + ( + 'Reads AND writes (CREATE/MERGE/SET/DELETE) are allowed.' + if self.IGlobal.allow_writes + else 'Read-only: write clauses (CREATE/MERGE/SET/DELETE) are rejected by the server.' + ) + + f' At most {self.IGlobal.max_rows} rows are returned.' + ), + ) + def query(self, args): + """Run Cypher against the selected graph.""" + args = normalize_tool_input(args, tool_name='falkordb') + cypher = args.get('cypher') + if not cypher or not isinstance(cypher, str) or not cypher.strip(): + raise ValueError('"cypher" is required and must be a non-empty string') + params = args.get('params') + if params is not None and not isinstance(params, dict): + raise ValueError('"params" must be an object when provided') + + try: + graph, _ = self._select_graph(args) + run = graph.query if self.IGlobal.allow_writes else graph.ro_query + result = run(cypher, params=params or None, timeout=self.IGlobal.query_timeout_ms) + except RedisError as e: + return {'error': str(e), 'columns': [], 'rows': [], 'row_count': 0, 'truncated': False} + + cap = self.IGlobal.max_rows + raw_rows = result.result_set or [] + rows = [[_serialize_value(cell) for cell in row] for row in raw_rows[:cap]] + + out = { + 'columns': _header_names(getattr(result, 'header', None)), + 'rows': rows, + 'row_count': len(rows), + 'truncated': len(raw_rows) > cap, + } + if self.IGlobal.allow_writes: + stats = _write_stats(result) + if stats: + out['stats'] = stats + return out + + @tool_function( + input_schema={'type': 'object', 'properties': {}}, + output_schema={ + 'type': 'object', + 'properties': { + 'graphs': {'type': 'array', 'items': {'type': 'string'}}, + 'error': {'type': 'string', 'description': 'Error message if the call failed.'}, + }, + }, + description='List the graph names that exist in this FalkorDB instance.', + ) + def list_graphs(self, args): + """List graphs on the server.""" + try: + graphs = self.IGlobal.client.list_graphs() + except RedisError as e: + return {'error': str(e), 'graphs': []} + return {'graphs': [str(g) for g in graphs or []]} + + @tool_function( + input_schema={ + 'type': 'object', + 'properties': { + 'graph': { + 'type': 'string', + 'description': 'Graph to inspect; defaults to the graph configured on the node.', + }, + }, + }, + output_schema={ + 'type': 'object', + 'properties': { + 'labels': {'type': 'array', 'items': {'type': 'string'}}, + 'relationship_types': {'type': 'array', 'items': {'type': 'string'}}, + 'property_keys': {'type': 'array', 'items': {'type': 'string'}}, + 'error': {'type': 'string', 'description': 'Error message if the call failed.'}, + }, + }, + description=( + 'Return the graph schema: node labels, relationship types and property keys. ' + 'Use when a query returns unexpected results or you need to discover the data model.' + ), + ) + def get_schema(self, args): + """Return labels, relationship types and property keys of a graph.""" + args = normalize_tool_input(args, tool_name='falkordb') + + def _column(graph, procedure: str) -> list: + result = graph.ro_query(f'CALL {procedure}()', timeout=self.IGlobal.query_timeout_ms) + return [str(row[0]) for row in (result.result_set or [])] + + try: + graph, _ = self._select_graph(args) + return { + 'labels': _column(graph, 'db.labels'), + 'relationship_types': _column(graph, 'db.relationshipTypes'), + 'property_keys': _column(graph, 'db.propertyKeys'), + } + except RedisError as e: + return {'error': str(e), 'labels': [], 'relationship_types': [], 'property_keys': []} diff --git a/nodes/src/nodes/tool_falkordb/README.md b/nodes/src/nodes/tool_falkordb/README.md new file mode 100644 index 000000000..1f3f200e3 --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/README.md @@ -0,0 +1,58 @@ +# FalkorDB Tool + +Lets agents query a [FalkorDB](https://www.falkordb.com) graph database with Cypher. Queries are **read-only by default** — they run via `GRAPH.RO_QUERY`, so the server itself rejects write clauses; flip *Allow Writes* to let the agent mutate the graph. Values always go through Cypher parameters (`$name`), which FalkorDB treats as data, never as query text. + +## Tools + +| Tool | Description | +| --------------------- | -------------------------------------------------------- | +| `falkordb.query` | Run a Cypher query and return rows | +| `falkordb.list_graphs`| List graph names on the server | +| `falkordb.get_schema` | Node labels, relationship types and property keys | + +### falkordb.query + +| Parameter | Required | Description | +| --------- | -------- | -------------------------------------------------------- | +| `cypher` | yes | Cypher query; reference values as `$name` | +| `params` | no | Values for the `$name` placeholders (injection-safe) | +| `graph` | no | Graph to query (default from config) | + +Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. + +### falkordb.list_graphs + +No parameters. Returns `graphs`. + +### falkordb.get_schema + +| Parameter | Required | Description | +| --------- | -------- | ------------------------------------ | +| `graph` | no | Graph to inspect (default from config) | + +Returns `labels`, `relationship_types` and `property_keys`. + +## Configuration + +| Field | Description | +| ----------------- | ---------------------------------------------------------------------- | +| Host / Port | FalkorDB endpoint (local Docker, self-hosted, or FalkorDB Cloud) | +| Username / Password | Credentials; FalkorDB Cloud uses `default` + instance password | +| TLS | Enable for TLS endpoints | +| Default Graph | Graph used when the agent does not pass one | +| Allow Writes | OFF = `GRAPH.RO_QUERY` (server rejects writes); ON = full read/write | +| Max Rows | Row cap per query returned to the agent | +| Query Timeout (ms)| Server-side per-query timeout | + +## Local quickstart + +```bash +docker run -p 6379:6379 -it --rm falkordb/falkordb:latest +``` + +Point the node at `localhost:6379` and ask the agent to `CREATE` (with *Allow Writes* on) or `MATCH` away. + +## Upstream docs + +- [FalkorDB documentation](https://docs.falkordb.com) +- [Cypher coverage](https://docs.falkordb.com/cypher/) diff --git a/nodes/src/nodes/tool_falkordb/__init__.py b/nodes/src/nodes/tool_falkordb/__init__.py new file mode 100644 index 000000000..d678f5ae5 --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/__init__.py @@ -0,0 +1,35 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +from os.path import dirname, join, realpath +from depends import depends + +requirements = join(dirname(realpath(__file__)), 'requirements.txt') +depends(requirements) + +from .IGlobal import IGlobal +from .IInstance import IInstance + +__all__ = ['IGlobal', 'IInstance'] diff --git a/nodes/src/nodes/tool_falkordb/doc.md b/nodes/src/nodes/tool_falkordb/doc.md new file mode 100644 index 000000000..12f366bb6 --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/doc.md @@ -0,0 +1,88 @@ +--- +title: FalkorDB +date: 2026-06-11 +sidebar_position: 1 +--- + +## What it does + +Lets agents query a [FalkorDB](https://www.falkordb.com) graph database with Cypher. Queries are **read-only by default** — they run via `GRAPH.RO_QUERY`, so the server itself rejects write clauses; flip *Allow Writes* to let the agent mutate the graph. Values always go through Cypher parameters (`$name`), which FalkorDB treats as data, never as query text. + +## Tools + +| Tool | Description | +| --------------------- | -------------------------------------------------------- | +| `falkordb.query` | Run a Cypher query and return rows | +| `falkordb.list_graphs`| List graph names on the server | +| `falkordb.get_schema` | Node labels, relationship types and property keys | + +### falkordb.query + +| Parameter | Required | Description | +| --------- | -------- | -------------------------------------------------------- | +| `cypher` | yes | Cypher query; reference values as `$name` | +| `params` | no | Values for the `$name` placeholders (injection-safe) | +| `graph` | no | Graph to query (default from config) | + +Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. + +### falkordb.list_graphs + +No parameters. Returns `graphs`. + +### falkordb.get_schema + +| Parameter | Required | Description | +| --------- | -------- | ------------------------------------ | +| `graph` | no | Graph to inspect (default from config) | + +Returns `labels`, `relationship_types` and `property_keys`. + +## Configuration + +| Field | Description | +| ----------------- | ---------------------------------------------------------------------- | +| Host / Port | FalkorDB endpoint (local Docker, self-hosted, or FalkorDB Cloud) | +| Username / Password | Credentials; FalkorDB Cloud uses `default` + instance password | +| TLS | Enable for TLS endpoints | +| Default Graph | Graph used when the agent does not pass one | +| Allow Writes | OFF = `GRAPH.RO_QUERY` (server rejects writes); ON = full read/write | +| Max Rows | Row cap per query returned to the agent | +| Query Timeout (ms)| Server-side per-query timeout | + +## Local quickstart + +```bash +docker run -p 6379:6379 -it --rm falkordb/falkordb:latest +``` + +Point the node at `localhost:6379` and ask the agent to `CREATE` (with *Allow Writes* on) or `MATCH` away. + +## Upstream docs + +- [FalkorDB documentation](https://docs.falkordb.com) +- [Cypher coverage](https://docs.falkordb.com/cypher/) + +## Reference + + + + +| Property | Value | +| --- | --- | +| Class type | tool | +| Capabilities | invoke | +| Protocol | `tool_falkordb://` | + +**Profiles** + +| Profile | Title | Model | +| --- | --- | --- | +| `default` | FalkorDB | | + +**Configuration sections** + +| Section | Fields | +| --- | --- | +| FalkorDB | `type`, `tool_falkordb.host`, `tool_falkordb.port`, `tool_falkordb.username`, `tool_falkordb.password`, `tool_falkordb.tls`, `tool_falkordb.graph`, `tool_falkordb.allow_writes`, `tool_falkordb.max_rows`, `tool_falkordb.query_timeout_ms` | + diff --git a/nodes/src/nodes/tool_falkordb/falkordb.svg b/nodes/src/nodes/tool_falkordb/falkordb.svg new file mode 100644 index 000000000..f6ee0c17b --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/falkordb.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/nodes/src/nodes/tool_falkordb/requirements.txt b/nodes/src/nodes/tool_falkordb/requirements.txt new file mode 100644 index 000000000..d09d9797d --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/requirements.txt @@ -0,0 +1 @@ +falkordb>=1.6,<2 diff --git a/nodes/src/nodes/tool_falkordb/services.json b/nodes/src/nodes/tool_falkordb/services.json new file mode 100644 index 000000000..b2ddf853e --- /dev/null +++ b/nodes/src/nodes/tool_falkordb/services.json @@ -0,0 +1,108 @@ +{ + "title": "FalkorDB", + "protocol": "tool_falkordb://", + "classType": ["tool"], + "capabilities": ["invoke"], + "register": "filter", + "node": "python", + "path": "nodes.tool_falkordb", + "prefix": "falkordb", + "icon": "falkordb.svg", + "documentation": "https://docs.rocketride.org", + "description": ["Lets agents query a FalkorDB graph database with Cypher.", "Provides query (read-only by default, server-enforced), list_graphs and get_schema."], + "tile": [], + "lanes": {}, + "preconfig": { + "default": "default", + "profiles": { + "default": { + "title": "FalkorDB" + } + } + }, + "fields": { + "tool_falkordb.host": { + "type": "string", + "title": "Host", + "description": "FalkorDB host, e.g. localhost or your-instance.falkordb.cloud.", + "default": "localhost" + }, + "tool_falkordb.port": { + "type": "integer", + "title": "Port", + "description": "FalkorDB port (Redis protocol).", + "default": 6379, + "minimum": 1, + "maximum": 65535 + }, + "tool_falkordb.username": { + "type": "string", + "title": "Username", + "description": "Username, e.g. \"default\" for FalkorDB Cloud. Leave empty for no auth.", + "default": "", + "optional": true + }, + "tool_falkordb.password": { + "type": "string", + "title": "Password", + "description": "Password for the FalkorDB instance. Leave empty for no auth.", + "default": "", + "secure": true, + "optional": true, + "ui": { + "ui:widget": "password" + } + }, + "tool_falkordb.tls": { + "type": "boolean", + "title": "TLS", + "description": "Connect with TLS (FalkorDB Cloud TLS endpoints).", + "default": false + }, + "tool_falkordb.graph": { + "type": "string", + "title": "Default Graph", + "description": "Graph queried when the agent does not pass one explicitly.", + "default": "agent" + }, + "tool_falkordb.allow_writes": { + "type": "boolean", + "title": "Allow Writes", + "description": "Permit CREATE/MERGE/SET/DELETE. When off, queries run via GRAPH.RO_QUERY and the server rejects write clauses.", + "default": false + }, + "tool_falkordb.max_rows": { + "type": "integer", + "title": "Max Rows", + "description": "Upper cap on rows returned to the agent per query.", + "default": 250, + "minimum": 1, + "maximum": 25000 + }, + "tool_falkordb.query_timeout_ms": { + "type": "integer", + "title": "Query Timeout (ms)", + "description": "Server-side timeout for a single query.", + "default": 30000, + "minimum": 100, + "maximum": 600000 + } + }, + "test": { + "profiles": ["default"], + "outputs": [], + "cases": [ + { + "name": "FalkorDB smoke test (mock client)", + "text": "test query" + } + ] + }, + "shape": [ + { + "section": "Pipe", + "title": "FalkorDB", + "properties": ["type", "tool_falkordb.host", "tool_falkordb.port", "tool_falkordb.username", "tool_falkordb.password", "tool_falkordb.tls", "tool_falkordb.graph", "tool_falkordb.allow_writes", "tool_falkordb.max_rows", "tool_falkordb.query_timeout_ms"] + } + ] +} diff --git a/nodes/test/mocks/falkordb/__init__.py b/nodes/test/mocks/falkordb/__init__.py new file mode 100644 index 000000000..cd179d089 --- /dev/null +++ b/nodes/test/mocks/falkordb/__init__.py @@ -0,0 +1,71 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Mock FalkorDB client for node testing — no real server is contacted.""" + +from __future__ import annotations + + +class _MockQueryResult: + def __init__(self, result_set=None, header=None): + self.result_set = result_set or [] + self.header = header or [] + self.nodes_created = 0 + self.nodes_deleted = 0 + self.relationships_created = 0 + self.relationships_deleted = 0 + self.properties_set = 0 + self.properties_removed = 0 + self.labels_added = 0 + self.indices_created = 0 + + +class _MockGraph: + def __init__(self, name): + self.name = name + + def query(self, q, params=None, timeout=None, **kwargs): + return _MockQueryResult(result_set=[['mock']], header=[[1, 'value']]) + + def ro_query(self, q, params=None, timeout=None, **kwargs): + return _MockQueryResult(result_set=[['mock']], header=[[1, 'value']]) + + +class FalkorDB: + def __init__(self, host='localhost', port=6379, username=None, password=None, ssl=False, **kwargs): + self.host = host + self.port = port + + def select_graph(self, name): + return _MockGraph(name) + + def list_graphs(self): + return ['mock-graph'] + + def close(self): + pass + + +__all__ = ['FalkorDB'] diff --git a/nodes/test/test_tool_falkordb.py b/nodes/test/test_tool_falkordb.py new file mode 100644 index 000000000..2a70c8f7b --- /dev/null +++ b/nodes/test/test_tool_falkordb.py @@ -0,0 +1,281 @@ +# ============================================================================= +# RocketRide Engine +# ============================================================================= +# MIT License +# Copyright (c) 2026 Aparavi Software AG +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ============================================================================= + +"""Unit tests for tool_falkordb helpers and tool-method behavior (no server).""" + +from __future__ import annotations + +import datetime +import importlib +import sys +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +# --------------------------------------------------------------------------- +# Bootstrap: when run under a bare interpreter that lacks the engine runtime +# (rocketlib, ai.common, falkordb, redis), inject lightweight stubs ONLY for +# modules that are not already present, import the module under test, then +# REMOVE the stubs we added so they never leak into the shared pytest session +# (see test_tool_tavily.py for the full rationale). +# --------------------------------------------------------------------------- + +_NODES_SRC = Path(__file__).resolve().parents[1] / 'src' +if str(_NODES_SRC) not in sys.path: + sys.path.insert(0, str(_NODES_SRC)) + + +class _StubRedisError(Exception): + """Real exception class so IInstance's except clauses catch it under the stub.""" + + +def _build_import_stubs(): + """Return {module_name: stub} for the deps needed only to import the module.""" + rocketlib = MagicMock() + rocketlib.IInstanceBase = object + rocketlib.IGlobalBase = object + rocketlib.tool_function = lambda **kwargs: lambda f: f + rocketlib.debug = lambda *a, **kw: None + rocketlib.error = lambda *a, **kw: None + rocketlib.warning = lambda *a, **kw: None + rocketlib.OPEN_MODE = MagicMock() + + depends = MagicMock() + depends.depends = lambda *a, **kw: None + + ai_common_utils = MagicMock() + ai_common_utils.normalize_tool_input = lambda args, **kw: args if isinstance(args, dict) else {} + + falkordb = MagicMock() + falkordb.FalkorDB = MagicMock() + + redis_exceptions = MagicMock() + redis_exceptions.RedisError = _StubRedisError + redis = MagicMock() + redis.exceptions = redis_exceptions + + return { + 'rocketlib': rocketlib, + 'depends': depends, + 'ai': MagicMock(), + 'ai.common': MagicMock(), + 'ai.common.utils': ai_common_utils, + 'ai.common.config': MagicMock(), + 'falkordb': falkordb, + 'redis': redis, + 'redis.exceptions': redis_exceptions, + } + + +_added_stubs = [] +for _name, _stub in _build_import_stubs().items(): + if _name not in sys.modules: + sys.modules[_name] = _stub + _added_stubs.append(_name) + +mod = importlib.import_module('nodes.tool_falkordb.IInstance') + +for _name in _added_stubs: + sys.modules.pop(_name, None) + + +class _FakeNode: + def __init__(self, node_id=1, labels=None, properties=None): + self.id = node_id + self.labels = labels or ['Person'] + self.properties = properties or {'name': 'Alice'} + + +class _FakeEdge: + def __init__(self): + self.id = 7 + self.relation = 'KNOWS' + self.src_node = 1 + self.dest_node = 2 + self.properties = {'since': 2020} + + +class _FakeResult: + def __init__(self, result_set=None, header=None, **stats): + self.result_set = result_set or [] + self.header = header or [] + for attr in ( + 'nodes_created', + 'nodes_deleted', + 'relationships_created', + 'relationships_deleted', + 'properties_set', + 'properties_removed', + 'labels_added', + 'indices_created', + ): + setattr(self, attr, stats.get(attr, 0)) + + +class _FakeGraph: + def __init__(self, result=None, raise_error=None): + self._result = result or _FakeResult() + self._raise = raise_error + self.calls = [] + + def query(self, q, params=None, timeout=None): + self.calls.append(('query', q, params, timeout)) + if self._raise: + raise self._raise + return self._result + + def ro_query(self, q, params=None, timeout=None): + self.calls.append(('ro_query', q, params, timeout)) + if self._raise: + raise self._raise + return self._result + + +class _FakeClient: + def __init__(self, graph): + self._graph = graph + self.selected = [] + + def select_graph(self, name): + self.selected.append(name) + return self._graph + + def list_graphs(self): + return ['g1', 'g2'] + + +class _FakeGlobal: + def __init__(self, graph, *, allow_writes=False, max_rows=250, graph_name='agent'): + self.client = _FakeClient(graph) + self.allow_writes = allow_writes + self.max_rows = max_rows + self.graph_name = graph_name + self.query_timeout_ms = 30000 + + +def _instance(global_state): + inst = mod.IInstance() + inst.IGlobal = global_state + return inst + + +# --------------------------------------------------------------------------- +# Serialization helpers +# --------------------------------------------------------------------------- + + +def test_serialize_node_edge_and_temporal(): + node = mod._serialize_value(_FakeNode()) + assert node == {'id': 1, 'labels': ['Person'], 'properties': {'name': 'Alice'}} + + edge = mod._serialize_value(_FakeEdge()) + assert edge == {'id': 7, 'type': 'KNOWS', 'src': 1, 'dst': 2, 'properties': {'since': 2020}} + + stamp = datetime.datetime(2026, 6, 11, 12, 0, 0) + assert mod._serialize_value(stamp) == '2026-06-11T12:00:00' + assert mod._serialize_value([1, 'a', None]) == [1, 'a', None] + + +def test_header_names_handles_pairs_and_strings(): + assert mod._header_names([[1, 'name'], [2, 'age']]) == ['name', 'age'] + assert mod._header_names(['plain']) == ['plain'] + assert mod._header_names(None) == [] + + +# --------------------------------------------------------------------------- +# query: routing, caps, errors +# --------------------------------------------------------------------------- + + +def test_query_uses_ro_query_when_writes_disabled(): + graph = _FakeGraph(_FakeResult(result_set=[['x']], header=[[1, 'value']])) + inst = _instance(_FakeGlobal(graph, allow_writes=False)) + out = inst.query({'cypher': 'MATCH (n) RETURN n'}) + assert graph.calls[0][0] == 'ro_query' + assert out['columns'] == ['value'] + assert out['row_count'] == 1 + + +def test_query_uses_query_when_writes_enabled_and_reports_stats(): + graph = _FakeGraph(_FakeResult(result_set=[], header=[], nodes_created=2)) + inst = _instance(_FakeGlobal(graph, allow_writes=True)) + out = inst.query({'cypher': 'CREATE (n) RETURN n'}) + assert graph.calls[0][0] == 'query' + assert out['stats'] == {'nodes_created': 2} + + +def test_query_caps_rows_and_flags_truncation(): + rows = [[i] for i in range(10)] + graph = _FakeGraph(_FakeResult(result_set=rows, header=[[1, 'n']])) + inst = _instance(_FakeGlobal(graph, max_rows=3)) + out = inst.query({'cypher': 'MATCH (n) RETURN n'}) + assert out['row_count'] == 3 + assert out['truncated'] is True + + +def test_query_rejects_bad_params_without_touching_client(): + graph = _FakeGraph() + glb = _FakeGlobal(graph) + inst = _instance(glb) + with pytest.raises(ValueError): + inst.query({'cypher': 'MATCH (n) RETURN n', 'params': 'not-a-dict'}) + assert graph.calls == [] + + +def test_query_returns_error_dict_on_redis_error(): + graph = _FakeGraph(raise_error=_StubRedisError('bad cypher')) + inst = _instance(_FakeGlobal(graph)) + out = inst.query({'cypher': 'MATCH (n) RETURN n'}) + assert out['error'] == 'bad cypher' + assert out['rows'] == [] + + +def test_query_graph_override_and_default(): + graph = _FakeGraph(_FakeResult()) + glb = _FakeGlobal(graph, graph_name='default-graph') + inst = _instance(glb) + inst.query({'cypher': 'MATCH (n) RETURN n'}) + inst.query({'cypher': 'MATCH (n) RETURN n', 'graph': 'other'}) + assert glb.client.selected == ['default-graph', 'other'] + + +# --------------------------------------------------------------------------- +# list_graphs / get_schema +# --------------------------------------------------------------------------- + + +def test_list_graphs_returns_names(): + inst = _instance(_FakeGlobal(_FakeGraph())) + assert inst.list_graphs({}) == {'graphs': ['g1', 'g2']} + + +def test_get_schema_shapes_columns(): + graph = _FakeGraph(_FakeResult(result_set=[['Person'], ['City']], header=[[1, 'label']])) + inst = _instance(_FakeGlobal(graph)) + out = inst.get_schema({}) + assert out['labels'] == ['Person', 'City'] + # All three procedures run read-only. + assert all(call[0] == 'ro_query' for call in graph.calls) From 617e25710301f34362df03b2ccb254902531c2ca Mon Sep 17 00:00:00 2001 From: Dmitrii Kataraev Date: Thu, 11 Jun 2026 16:59:13 -0700 Subject: [PATCH 2/6] fix(tool_falkordb): module-bound RedisError in tests, current-state comment, README mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error-path test raised a file-local stub exception while the module under test binds RedisError at import — with real redis already in sys.modules (the nodes:test-full condition) the except clause no longer caught it. The fake now raises mod.RedisError, which is correct under both stubbed and real imports (verified by running with real redis preimported). memory_persistent's requirements comment describes the current constraint only; README mirrors doc.md per the tool_apify convention. Co-Authored-By: Claude Fable 5 --- nodes/src/nodes/memory_persistent/requirements.txt | 7 +++---- nodes/src/nodes/tool_falkordb/README.md | 8 +++++++- nodes/test/test_tool_falkordb.py | 5 ++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/nodes/src/nodes/memory_persistent/requirements.txt b/nodes/src/nodes/memory_persistent/requirements.txt index 662854fc8..e7818aaa4 100644 --- a/nodes/src/nodes/memory_persistent/requirements.txt +++ b/nodes/src/nodes/memory_persistent/requirements.txt @@ -1,5 +1,4 @@ -# falkordb (any version) needs redis>=7.1 or <6; the previous >=6.4,<7 pin made -# the engine-wide constraints solve unsatisfiable. The redis-py surface this -# node uses (get/exists/hget/incrby/lrange/pexpire/pipeline/pttl/rpush/sadd/ -# scard/sismember/smembers/srem) is identical across 6.x and 7.x. +# falkordb requires redis>=7.1. The redis-py surface this node uses +# (get/exists/hget/hset/delete/incrby/lrange/pexpire/pipeline/pttl/rpush/sadd/ +# scard/sismember/smembers/srem) is identical across redis-py 6.x and 7.x. redis>=7.1,<8 diff --git a/nodes/src/nodes/tool_falkordb/README.md b/nodes/src/nodes/tool_falkordb/README.md index 1f3f200e3..1e058aa8f 100644 --- a/nodes/src/nodes/tool_falkordb/README.md +++ b/nodes/src/nodes/tool_falkordb/README.md @@ -1,4 +1,10 @@ -# FalkorDB Tool +--- +title: FalkorDB +date: 2026-06-11 +sidebar_position: 1 +--- + +## What it does Lets agents query a [FalkorDB](https://www.falkordb.com) graph database with Cypher. Queries are **read-only by default** — they run via `GRAPH.RO_QUERY`, so the server itself rejects write clauses; flip *Allow Writes* to let the agent mutate the graph. Values always go through Cypher parameters (`$name`), which FalkorDB treats as data, never as query text. diff --git a/nodes/test/test_tool_falkordb.py b/nodes/test/test_tool_falkordb.py index 2a70c8f7b..4f39602b5 100644 --- a/nodes/test/test_tool_falkordb.py +++ b/nodes/test/test_tool_falkordb.py @@ -246,7 +246,10 @@ def test_query_rejects_bad_params_without_touching_client(): def test_query_returns_error_dict_on_redis_error(): - graph = _FakeGraph(raise_error=_StubRedisError('bad cypher')) + # Raise the class the module actually bound — under `builder nodes:test-full` + # the real redis may already be imported, and the file-local stub would + # then not be caught by IInstance's `except RedisError`. + graph = _FakeGraph(raise_error=mod.RedisError('bad cypher')) inst = _instance(_FakeGlobal(graph)) out = inst.query({'cypher': 'MATCH (n) RETURN n'}) assert out['error'] == 'bad cypher' From 293c4b22c9cbe4e624caa6e43a9df68e8bec2d24 Mon Sep 17 00:00:00 2001 From: Dmitrii Kataraev Date: Thu, 11 Jun 2026 17:24:44 -0700 Subject: [PATCH 3/6] docs(tool_falkordb): state the error return contract for all three tools Co-Authored-By: Claude Fable 5 --- nodes/src/nodes/tool_falkordb/README.md | 2 +- nodes/src/nodes/tool_falkordb/doc.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/src/nodes/tool_falkordb/README.md b/nodes/src/nodes/tool_falkordb/README.md index 1e058aa8f..aa6ed5816 100644 --- a/nodes/src/nodes/tool_falkordb/README.md +++ b/nodes/src/nodes/tool_falkordb/README.md @@ -24,7 +24,7 @@ Lets agents query a [FalkorDB](https://www.falkordb.com) graph database with Cyp | `params` | no | Values for the `$name` placeholders (injection-safe) | | `graph` | no | Graph to query (default from config) | -Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. +Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. On a query failure all three tools return `error` instead (with empty `rows`/`columns`, `row_count: 0`, `truncated: false` for `query`). ### falkordb.list_graphs diff --git a/nodes/src/nodes/tool_falkordb/doc.md b/nodes/src/nodes/tool_falkordb/doc.md index 12f366bb6..a324c580f 100644 --- a/nodes/src/nodes/tool_falkordb/doc.md +++ b/nodes/src/nodes/tool_falkordb/doc.md @@ -24,7 +24,7 @@ Lets agents query a [FalkorDB](https://www.falkordb.com) graph database with Cyp | `params` | no | Values for the `$name` placeholders (injection-safe) | | `graph` | no | Graph to query (default from config) | -Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. +Returns `columns`, `rows` (nodes/edges serialized to objects, capped at *Max Rows* with a `truncated` flag) and, when writes are enabled, non-zero write `stats`. On a query failure all three tools return `error` instead (with empty `rows`/`columns`, `row_count: 0`, `truncated: false` for `query`). ### falkordb.list_graphs From 8f6fe5a28d8b28764d399c2a62285a532e41c363 Mon Sep 17 00:00:00 2001 From: Dmitrii Kataraev Date: Thu, 11 Jun 2026 18:04:18 -0700 Subject: [PATCH 4/6] chore(tool_falkordb): declare the redis dependency the node imports IInstance catches redis.exceptions.RedisError (falkordb-py re-raises redis-py exceptions verbatim and exports no error hierarchy of its own), so the import gets an explicit requirement instead of leaning on falkordb's transitive dependency. Same range the solve already pins. Co-Authored-By: Claude Fable 5 --- nodes/src/nodes/tool_falkordb/requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nodes/src/nodes/tool_falkordb/requirements.txt b/nodes/src/nodes/tool_falkordb/requirements.txt index d09d9797d..adf28423a 100644 --- a/nodes/src/nodes/tool_falkordb/requirements.txt +++ b/nodes/src/nodes/tool_falkordb/requirements.txt @@ -1 +1,4 @@ falkordb>=1.6,<2 +# Declared because IInstance imports redis.exceptions directly: falkordb-py +# has no error hierarchy of its own and re-raises redis-py exceptions verbatim. +redis>=7.1,<8 From 57d48747bef53214db363944858bca25bf868f14 Mon Sep 17 00:00:00 2001 From: Alexandru Sclearuc Date: Fri, 12 Jun 2026 05:09:32 +0300 Subject: [PATCH 5/6] fix(nodes): marking as experimental --- nodes/src/nodes/tool_falkordb/services.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/src/nodes/tool_falkordb/services.json b/nodes/src/nodes/tool_falkordb/services.json index b2ddf853e..13850efae 100644 --- a/nodes/src/nodes/tool_falkordb/services.json +++ b/nodes/src/nodes/tool_falkordb/services.json @@ -2,7 +2,7 @@ "title": "FalkorDB", "protocol": "tool_falkordb://", "classType": ["tool"], - "capabilities": ["invoke"], + "capabilities": ["invoke", "experimental"], "register": "filter", "node": "python", "path": "nodes.tool_falkordb", From 064da9b17ba7478544c7ec92d0e9b288f4b7362e Mon Sep 17 00:00:00 2001 From: Alexandru Sclearuc Date: Fri, 12 Jun 2026 05:12:32 +0300 Subject: [PATCH 6/6] fix(nodes): removing version constraints --- nodes/src/nodes/memory_persistent/requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/nodes/src/nodes/memory_persistent/requirements.txt b/nodes/src/nodes/memory_persistent/requirements.txt index e7818aaa4..7800f0fad 100644 --- a/nodes/src/nodes/memory_persistent/requirements.txt +++ b/nodes/src/nodes/memory_persistent/requirements.txt @@ -1,4 +1 @@ -# falkordb requires redis>=7.1. The redis-py surface this node uses -# (get/exists/hget/hset/delete/incrby/lrange/pexpire/pipeline/pttl/rpush/sadd/ -# scard/sismember/smembers/srem) is identical across redis-py 6.x and 7.x. -redis>=7.1,<8 +redis