memgraph-toolbox 0.2.1__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/.gitignore +3 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/PKG-INFO +1 -1
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/pyproject.toml +1 -1
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/memgraph.py +48 -4
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/client/mcp_prompt.py +14 -8
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_async_memgraph.py +5 -0
- memgraph_toolbox-0.2.3/src/memgraph_toolbox/tests/test_merge_with_retry.py +69 -0
- memgraph_toolbox-0.2.1/uv.lock +0 -3399
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/LICENSE +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/README.md +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/tool.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/api/toolbox.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/client/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/evals/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/evals/coherence.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/memgraph_toolbox.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/py.typed +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_embedding.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_eval_coherence.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_mcp_prompt.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_memgraph_config.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_toolbox.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_tools.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/__init__.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/cypher.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tools/schema.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/embedding.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/logger.py +0 -0
- {memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/utils/serialization.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: memgraph-toolbox
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Memgraph toolbox library for Memgraph AI tools and utilities
|
|
5
5
|
Project-URL: Homepage, https://github.com/memgraph/ai-toolkit
|
|
6
6
|
Project-URL: Source, https://github.com/memgraph/ai-toolkit
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import time
|
|
2
3
|
from collections.abc import Mapping
|
|
3
4
|
from typing import Any, cast
|
|
4
5
|
|
|
@@ -78,6 +79,49 @@ def memgraph_env(
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
def merge_with_retry(db: Any, query: str, params: dict | None = None, *, attempts: int = 3) -> list[dict[str, Any]]:
|
|
83
|
+
"""Run *query*, a MERGE of a uniqueness-constrained node, through *db*, surviving a concurrent create.
|
|
84
|
+
|
|
85
|
+
Two transactions that MERGE the same new node both find it missing and
|
|
86
|
+
both create it; the later commit fails the uniqueness constraint. Run
|
|
87
|
+
again, its MERGE matches the node the other one committed. Writers that
|
|
88
|
+
race this way are typically one process per event (agent hooks).
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
db: Anything with ``query(query, params)``, normally a :class:`Memgraph`.
|
|
92
|
+
query: The Cypher to run; it must be safe to run more than once.
|
|
93
|
+
params: Its parameters.
|
|
94
|
+
attempts: Runs before giving up, the first included.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The rows of the run that committed.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
neo4j.exceptions.ClientError: a uniqueness violation on the last
|
|
101
|
+
attempt, or any other client error at once.
|
|
102
|
+
"""
|
|
103
|
+
from neo4j.exceptions import ClientError
|
|
104
|
+
|
|
105
|
+
for attempt in range(attempts):
|
|
106
|
+
try:
|
|
107
|
+
return db.query(query, params=params)
|
|
108
|
+
except ClientError as exc:
|
|
109
|
+
if "unique constraint" not in str(exc) or attempt == attempts - 1:
|
|
110
|
+
raise
|
|
111
|
+
time.sleep(0.05 * (attempt + 1))
|
|
112
|
+
raise ValueError(f"attempts must be at least 1, got {attempts}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _driver_database(name: str) -> str | None:
|
|
116
|
+
"""The database to hand the driver: *name*, or None for an empty one (the login's main database).
|
|
117
|
+
|
|
118
|
+
Over a direct ``bolt://`` connection ``""`` and None both leave the database to the
|
|
119
|
+
server. A routing ``neo4j://`` driver instead asks the cluster for the routing table
|
|
120
|
+
of a database named ``""``, which has no writer, so every write fails.
|
|
121
|
+
"""
|
|
122
|
+
return name or None
|
|
123
|
+
|
|
124
|
+
|
|
81
125
|
class Memgraph:
|
|
82
126
|
"""
|
|
83
127
|
Base Memgraph client for interacting with Memgraph database.
|
|
@@ -107,7 +151,7 @@ class Memgraph:
|
|
|
107
151
|
url: The Memgraph connection URL
|
|
108
152
|
username: Username for authentication
|
|
109
153
|
password: Password for authentication
|
|
110
|
-
database: The database name to connect to (default: "memgraph")
|
|
154
|
+
database: The database name to connect to (default: "memgraph"); "" for the login's main database
|
|
111
155
|
driver_config: Additional Neo4j driver configuration
|
|
112
156
|
user_agent: Client name sent to the server (e.g. "mcp-memgraph", "langchain-memgraph", "sql2graph", etc.)
|
|
113
157
|
"""
|
|
@@ -123,7 +167,7 @@ class Memgraph:
|
|
|
123
167
|
|
|
124
168
|
self.driver = GraphDatabase.driver(url, auth=(username, password), **config)
|
|
125
169
|
|
|
126
|
-
self.database = env["MEMGRAPH_DATABASE"]
|
|
170
|
+
self.database = _driver_database(env["MEMGRAPH_DATABASE"])
|
|
127
171
|
|
|
128
172
|
try:
|
|
129
173
|
import neo4j
|
|
@@ -217,7 +261,7 @@ class AsyncMemgraph:
|
|
|
217
261
|
url: The Memgraph connection URL
|
|
218
262
|
username: Username for authentication
|
|
219
263
|
password: Password for authentication
|
|
220
|
-
database: The database name to connect to (default: "memgraph")
|
|
264
|
+
database: The database name to connect to (default: "memgraph"); "" for the login's main database
|
|
221
265
|
driver_config: Additional Neo4j driver configuration
|
|
222
266
|
user_agent: Client name sent to the server (e.g. "mcp-memgraph", "langchain-memgraph", "sql2graph", etc.)
|
|
223
267
|
"""
|
|
@@ -232,7 +276,7 @@ class AsyncMemgraph:
|
|
|
232
276
|
config.setdefault("user_agent", user_agent or self.DEFAULT_USER_AGENT)
|
|
233
277
|
|
|
234
278
|
self.driver = AsyncGraphDatabase.driver(url, auth=(username, password), **config)
|
|
235
|
-
self.database = env["MEMGRAPH_DATABASE"]
|
|
279
|
+
self.database = _driver_database(env["MEMGRAPH_DATABASE"])
|
|
236
280
|
|
|
237
281
|
async def verify_connectivity(self) -> None:
|
|
238
282
|
"""
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
from typing import cast
|
|
7
7
|
|
|
8
8
|
try:
|
|
9
|
-
from litellm import acompletion, experimental_mcp_client
|
|
9
|
+
from litellm import ModelResponse, acompletion, experimental_mcp_client
|
|
10
10
|
from mcp import ClientSession, StdioServerParameters
|
|
11
11
|
from mcp.client.stdio import stdio_client
|
|
12
12
|
except ImportError as e:
|
|
@@ -69,10 +69,13 @@ async def prompt_with_tools(
|
|
|
69
69
|
logger.info(f"{len(tools)} MCP tools loaded")
|
|
70
70
|
|
|
71
71
|
messages = [*tool_selection_system_messages, {"role": "user", "content": prompt}]
|
|
72
|
-
resp =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
resp = cast(
|
|
73
|
+
"ModelResponse",
|
|
74
|
+
await acompletion(
|
|
75
|
+
model=tool_selection_model_name,
|
|
76
|
+
messages=messages,
|
|
77
|
+
tools=tools,
|
|
78
|
+
),
|
|
76
79
|
)
|
|
77
80
|
msg = resp["choices"][0]["message"]
|
|
78
81
|
tool_calls = msg.get("tool_calls", [])
|
|
@@ -147,9 +150,12 @@ async def prompt_with_tools(
|
|
|
147
150
|
)
|
|
148
151
|
|
|
149
152
|
messages = messages + response_system_messages
|
|
150
|
-
final_resp =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
final_resp = cast(
|
|
154
|
+
"ModelResponse",
|
|
155
|
+
await acompletion(
|
|
156
|
+
model=response_model_name,
|
|
157
|
+
messages=messages,
|
|
158
|
+
),
|
|
153
159
|
)
|
|
154
160
|
return final_resp["choices"][0]["message"].content
|
|
155
161
|
|
{memgraph_toolbox-0.2.1 → memgraph_toolbox-0.2.3}/src/memgraph_toolbox/tests/test_async_memgraph.py
RENAMED
|
@@ -32,6 +32,11 @@ def test_async_memgraph_resolves_config_from_env(monkeypatch):
|
|
|
32
32
|
assert explicit.database == "arg-db"
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
def test_an_empty_database_reaches_the_driver_as_none():
|
|
36
|
+
"""An empty name means the login's main database; a routing driver would look up a database named ""."""
|
|
37
|
+
assert AsyncMemgraph(url="neo4j://localhost:7687", database="").database is None
|
|
38
|
+
|
|
39
|
+
|
|
35
40
|
def test_async_memgraph_mirrors_sync_defaults():
|
|
36
41
|
"""AsyncMemgraph shares the sync client's user-agent default."""
|
|
37
42
|
assert AsyncMemgraph.DEFAULT_USER_AGENT == Memgraph.DEFAULT_USER_AGENT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Tests for merge_with_retry against a live Memgraph; they skip when none is reachable."""
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from neo4j.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
from ..api.memgraph import Memgraph, merge_with_retry
|
|
9
|
+
|
|
10
|
+
_LABEL = "MergeWithRetryProbe"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def db():
|
|
15
|
+
try:
|
|
16
|
+
client = Memgraph()
|
|
17
|
+
except Exception as exc:
|
|
18
|
+
pytest.skip(f"No Memgraph server reachable at the configured MEMGRAPH_URL: {exc}")
|
|
19
|
+
client.query(f"MATCH (n:{_LABEL}) DETACH DELETE n")
|
|
20
|
+
client.query(f"CREATE CONSTRAINT ON (n:{_LABEL}) ASSERT n.key IS UNIQUE;")
|
|
21
|
+
yield client
|
|
22
|
+
client.query(f"MATCH (n:{_LABEL}) DETACH DELETE n")
|
|
23
|
+
client.query(f"DROP CONSTRAINT ON (n:{_LABEL}) ASSERT n.key IS UNIQUE;")
|
|
24
|
+
client.close()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_concurrent_merges_of_new_nodes_converge_on_one_node_each(db):
|
|
28
|
+
keys = [f"k{i}" for i in range(10)]
|
|
29
|
+
|
|
30
|
+
def merge(key: str) -> None:
|
|
31
|
+
merge_with_retry(db, f"MERGE (n:{_LABEL} {{key: $key}})", {"key": key})
|
|
32
|
+
|
|
33
|
+
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
34
|
+
list(pool.map(merge, keys * 16))
|
|
35
|
+
|
|
36
|
+
rows = db.query(f"MATCH (n:{_LABEL}) RETURN n.key AS key, count(*) AS c ORDER BY key")
|
|
37
|
+
assert rows == [{"key": key, "c": 1} for key in sorted(keys)]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_returns_the_rows_of_the_run_that_committed(db):
|
|
41
|
+
rows = merge_with_retry(db, f"MERGE (n:{_LABEL} {{key: 'a'}}) RETURN n.key AS key")
|
|
42
|
+
assert rows == [{"key": "a"}]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_any_other_client_error_is_raised_without_retrying(db):
|
|
46
|
+
calls = []
|
|
47
|
+
|
|
48
|
+
class Counting:
|
|
49
|
+
def query(self, query, params=None):
|
|
50
|
+
calls.append(query)
|
|
51
|
+
return db.query(query, params)
|
|
52
|
+
|
|
53
|
+
with pytest.raises(ClientError):
|
|
54
|
+
merge_with_retry(Counting(), "MERGE (n:")
|
|
55
|
+
assert len(calls) == 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_a_conflict_on_every_attempt_is_raised_after_the_last():
|
|
59
|
+
"""A conflict that never clears can't be produced on demand by a real server."""
|
|
60
|
+
calls = []
|
|
61
|
+
|
|
62
|
+
class AlwaysConflicting:
|
|
63
|
+
def query(self, query, params=None):
|
|
64
|
+
calls.append(query)
|
|
65
|
+
raise ClientError("Unable to commit due to unique constraint violation on :X(key)")
|
|
66
|
+
|
|
67
|
+
with pytest.raises(ClientError, match="unique constraint"):
|
|
68
|
+
merge_with_retry(AlwaysConflicting(), "MERGE (n:X {key: 1})", attempts=2)
|
|
69
|
+
assert len(calls) == 2
|